@futdevpro/fsm-dynamo 1.12.10 → 1.12.11

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.
Files changed (36) hide show
  1. package/build/_collections/utils/async.util.d.ts +4 -3
  2. package/build/_collections/utils/async.util.d.ts.map +1 -1
  3. package/build/_collections/utils/async.util.js +65 -3
  4. package/build/_collections/utils/async.util.js.map +1 -1
  5. package/build/_collections/utils/time.util.js +7 -7
  6. package/build/_collections/utils/time.util.js.map +1 -1
  7. package/build/_models/interfaces/search-query.interface.d.ts +11 -0
  8. package/build/_models/interfaces/search-query.interface.d.ts.map +1 -1
  9. package/build/_models/types/db-/304/221filter.type.d.ts +2 -1
  10. package/build/_models/types/db-/304/221filter.type.d.ts.map +1 -1
  11. package/build/_models/types/db-/304/221filter.type.js.map +1 -1
  12. package/build/_modules/crypto/_collections/crypto-old.util.d.ts +107 -0
  13. package/build/_modules/crypto/_collections/crypto-old.util.d.ts.map +1 -0
  14. package/build/_modules/crypto/_collections/crypto-old.util.js +279 -0
  15. package/build/_modules/crypto/_collections/crypto-old.util.js.map +1 -0
  16. package/build/_modules/crypto/_collections/crypto.util.d.ts +38 -6
  17. package/build/_modules/crypto/_collections/crypto.util.d.ts.map +1 -1
  18. package/build/_modules/crypto/_collections/crypto.util.js +298 -36
  19. package/build/_modules/crypto/_collections/crypto.util.js.map +1 -1
  20. package/build/_modules/crypto/_collections/crypto.util.spec.js +397 -2
  21. package/build/_modules/crypto/_collections/crypto.util.spec.js.map +1 -1
  22. package/build/_modules/crypto/index.d.ts +1 -1
  23. package/build/_modules/crypto/index.d.ts.map +1 -1
  24. package/build/_modules/crypto/index.js +1 -1
  25. package/build/_modules/crypto/index.js.map +1 -1
  26. package/futdevpro-fsm-dynamo-01.12.11.tgz +0 -0
  27. package/package.json +1 -1
  28. package/src/_collections/utils/async.util.ts +65 -4
  29. package/src/_collections/utils/time.util.ts +7 -7
  30. package/src/_models/interfaces/search-query.interface.ts +11 -0
  31. package/src/_models/types/db-/304/221filter.type.ts +6 -5
  32. package/src/_modules/crypto/_collections/crypto-old.util.ts +323 -0
  33. package/src/_modules/crypto/_collections/crypto.util.spec.ts +475 -2
  34. package/src/_modules/crypto/_collections/crypto.util.ts +337 -43
  35. package/src/_modules/crypto/index.ts +1 -1
  36. package/futdevpro-fsm-dynamo-01.12.10.tgz +0 -0
@@ -1,5 +1,7 @@
1
1
  import { Observable } from 'rxjs';
2
-
2
+ import { second, hour, minute, week, day } from '../constants/times.const';
3
+ import { DyFM_Time } from './time.util';
4
+ import { DyFM_Log } from './log.util';
3
5
 
4
6
 
5
7
  export class DyFM_Async {
@@ -9,11 +11,11 @@ export class DyFM_Async {
9
11
  * @param ms
10
12
  * @returns
11
13
  */
12
- static delay(ms: number): Promise<void> {
14
+ static wait(ms: number): Promise<void> {
13
15
  return new Promise((resolve): any => setTimeout(resolve, ms));
14
16
  }
15
- static readonly sleep: typeof this.delay = this.delay;
16
- static readonly wait: typeof this.delay = this.delay;
17
+ static readonly sleep: typeof this.wait = this.wait;
18
+ static readonly delay: typeof this.wait = this.wait;
17
19
 
18
20
  /**
19
21
  * WARNING: This function is recommended to use ONLY for limited instances,
@@ -94,4 +96,63 @@ export class DyFM_Async {
94
96
  );
95
97
  });
96
98
  }
99
+
100
+ static async waitWithCountdownLogging(
101
+ totalWaitTimeMs: number,
102
+ context: string = 'Countdown'
103
+ ): Promise<void> {
104
+ const startTime = Date.now();
105
+ const endTime = startTime + totalWaitTimeMs;
106
+
107
+ // Define logging intervals based on total duration
108
+ const getLogInterval = (totalMs: number): number => {
109
+ const totalSeconds = totalMs / second;
110
+ const totalMinutes = totalSeconds / 60;
111
+ const totalHours = totalMinutes / 60;
112
+ const totalDays = totalHours / 24;
113
+ const totalWeeks = totalDays / 7;
114
+
115
+ if (totalWeeks >= 4) return week; // Every week
116
+ if (totalDays >= 7) return day; // Every day
117
+ if (totalDays >= 3) return 12 * hour; // Every 12 hours
118
+ if (totalDays >= 1) return 6 * hour; // Every 6 hours
119
+ if (totalHours >= 12) return 3 * hour; // Every 3 hours
120
+ if (totalHours >= 6) return hour; // Every hour
121
+ if (totalHours >= 3) return 30 * minute; // Every 30 minutes
122
+ if (totalHours >= 1) return 10 * minute; // Every 10 minutes
123
+ if (totalMinutes >= 30) return 5 * minute; // Every 5 minutes
124
+ if (totalMinutes >= 10) return minute; // Every minute
125
+ if (totalMinutes >= 5) return 30 * second; // Every 30 seconds
126
+ if (totalMinutes >= 1) return 10 * second; // Every 10 seconds
127
+ if (totalSeconds >= 30) return 5 * second; // Every 5 seconds
128
+ return second; // Every second
129
+ };
130
+
131
+ const logInterval = getLogInterval(totalWaitTimeMs);
132
+ let lastLogTime = startTime;
133
+
134
+ // Format remaining time for display using DyFM_Time utility
135
+ const formatRemainingTime = (remainingMs: number): string => DyFM_Time.getTimeInShortestString(remainingMs) as string;
136
+
137
+ // Log initial countdown start
138
+ DyFM_Log.info(`⏳ ${context} started - Total duration: ${formatRemainingTime(totalWaitTimeMs)}`);
139
+
140
+ while (Date.now() < endTime) {
141
+ const now = Date.now();
142
+ const remainingMs = endTime - now;
143
+
144
+ // Check if it's time to log
145
+ if (now - lastLogTime >= logInterval) {
146
+ const remainingTime = formatRemainingTime(remainingMs);
147
+ DyFM_Log.info(`⏳ ${context} - ${remainingTime} remaining`);
148
+ lastLogTime = now;
149
+ }
150
+
151
+ // Wait for a short interval before checking again
152
+ await DyFM_Async.wait(Math.min(DyFM_Time.second, remainingMs));
153
+ }
154
+
155
+ // Log completion
156
+ DyFM_Log.success(`✅ ${context} completed`);
157
+ }
97
158
  }
@@ -258,19 +258,19 @@ export class DyFM_Time {
258
258
  } else if (duration < second) {
259
259
  return `${duration}ms`;
260
260
  } else if (duration < minute) {
261
- return `${DyFM_Math.round(duration / second)}s ${duration % second}ms`;
261
+ return `${DyFM_Math.round(duration / second, 0)}s ${duration % second}ms`;
262
262
  } else if (duration < hour) {
263
- return `${DyFM_Math.round(duration / minute)}m ${DyFM_Math.round(duration % minute)}s`;
263
+ return `${DyFM_Math.round(duration / minute, 0)}m ${DyFM_Math.round(duration % minute)}s`;
264
264
  } else if (duration < day) {
265
- return `${DyFM_Math.round(duration / hour)}h ${DyFM_Math.round(duration % hour)}m`;
265
+ return `${DyFM_Math.round(duration / hour, 0)}h ${DyFM_Math.round(duration % hour)}m`;
266
266
  } else if (duration < week) {
267
- return `${DyFM_Math.round(duration / day)}d ${DyFM_Math.round(duration % day)}h`;
267
+ return `${DyFM_Math.round(duration / day, 0)}d ${DyFM_Math.round(duration % day)}h`;
268
268
  } else if (duration < month) {
269
- return `${DyFM_Math.round(duration / week)}w ${DyFM_Math.round(duration % week)}d`;
269
+ return `${DyFM_Math.round(duration / week, 0)}w ${DyFM_Math.round(duration % week)}d`;
270
270
  } else if (duration < year) {
271
- return `${DyFM_Math.round(duration / month)}m ${DyFM_Math.round(duration % month)}w`;
271
+ return `${DyFM_Math.round(duration / month, 0)}m ${DyFM_Math.round(duration % month)}w`;
272
272
  } else {
273
- return `${DyFM_Math.round(duration / year)}y ${DyFM_Math.round(duration % year)}m`;
273
+ return `${DyFM_Math.round(duration / year, 0)}y ${DyFM_Math.round(duration % year)}m`;
274
274
  }
275
275
  }
276
276
 
@@ -5,8 +5,19 @@ import { DyFM_DBĐSort } from '../types/db-đsort.type';
5
5
 
6
6
  export interface DyFM_SearchQuery<T> {
7
7
  filterBy?: DyFM_DBĐFilter<T>; //DyFM_DBFilterSimple<T>; //DyFM_DBFilter<T>;
8
+ /**
9
+ * The sort order to load.
10
+ * The last sort will apply last, so it will be the strongest sort.
11
+ */
8
12
  sortBy?: DyFM_DBĐSort[];
13
+ /**
14
+ * The page index number to load.
15
+ * 0-indexed.
16
+ */
9
17
  page?: number;
18
+ /**
19
+ * The page size to load.
20
+ */
10
21
  pageSize?: number;
11
22
  }
12
23
 
@@ -9,12 +9,13 @@ import { DyFM_RangeValue } from '../control-models/range-value.control-model';
9
9
  * it should be the value, a range or an array of the searching values
10
10
  */
11
11
  export type DyFM_DBĐFilter<T> = {
12
- [K in keyof T]?: T[K] |
13
- T[K][] |
14
- DyFM_RangeValue<T[K]> |
15
- DyFM_SpecialSearch<T[K]> |
16
- DyFM_SpecialNestSearch;
12
+ [K in keyof T]?: DyFM_DBĐFilterProperty<T, K>;
17
13
  };
14
+ export type DyFM_DBĐFilterProperty<T, K extends keyof T> = T[K] |
15
+ T[K][] |
16
+ DyFM_RangeValue<T[K]> |
17
+ DyFM_SpecialSearch<T[K]> |
18
+ DyFM_SpecialNestSearch;
18
19
 
19
20
  export interface DyFM_SpecialSearch<T> {
20
21
  isSpecialSearch: true;
@@ -0,0 +1,323 @@
1
+ import * as CryptoJS from 'crypto-js';
2
+ import {
3
+ DyFM_Error,
4
+ DyFM_Error_Settings
5
+ } from '../../../_models/control-models/error.control-model';
6
+ import { DyFM_Object } from '../../../_collections/utils/object.util';
7
+
8
+
9
+ /**
10
+ * Configuration options for encryption/decryption
11
+ */
12
+ export interface CryptoConfig {
13
+ ivLength?: number;
14
+ saltLength?: number;
15
+ keyIterations?: number;
16
+ keySize?: number;
17
+ }
18
+
19
+ // Compact: about 60–80 character tokens, not 200+
20
+ // Non-standard: hard to reverse-engineer
21
+ // Usable in cookies, headers, URLs
22
+
23
+ /**
24
+ * A utility class for stable encryption and decryption of data
25
+ * Uses AES-256-CBC with deterministic IV and salt for consistent results across systems
26
+ * Prioritizes reliability and cross-platform compatibility over security
27
+ *
28
+ * @important DETERMINISTIC ENCRYPTION: This implementation produces identical encrypted
29
+ * output for identical input data and key across different systems and multiple calls.
30
+ * The same input will ALWAYS generate the same encrypted string on any platform.
31
+ *
32
+ * @warning SECURITY NOTICE: This deterministic behavior is intentional for cross-platform
33
+ * compatibility but reduces security. Identical inputs produce identical outputs, which
34
+ * can be exploited for pattern analysis attacks. Use only when consistency across
35
+ * systems is more important than cryptographic security.
36
+ */
37
+ export class DyFM_Crypto {
38
+ private static readonly DEFAULT_CONFIG: Required<CryptoConfig> = {
39
+ ivLength: 16, // 128 bits
40
+ saltLength: 16, // 128 bits
41
+ keyIterations: 1000, // Reduced for better performance and stability
42
+ keySize: 8 // 256 bits (8 * 32)
43
+ };
44
+ private static readonly defaultErrorUserMsg =
45
+ `We encountered an unhandled Authentication Error, ` +
46
+ `\nplease contact the responsible development team.`;
47
+
48
+ /**
49
+ * Validates the input data and key
50
+ * @throws {DyFM_Error} if validation fails
51
+ */
52
+ private static validateInput(data: any, key: string): void {
53
+ if (!key || typeof key !== 'string' || key.trim().length === 0) {
54
+ throw new DyFM_Error({
55
+ ...this.getDefaultErrorSettings('validateInput'),
56
+ errorCode: 'DyFM-CRY-IKY',
57
+ message: 'Invalid encryption key'
58
+ });
59
+ }
60
+
61
+ // Allow null values but not undefined
62
+ if (data === undefined) {
63
+ throw new DyFM_Error({
64
+ ...this.getDefaultErrorSettings('validateInput'),
65
+ errorCode: 'DyFM-CRY-IDT',
66
+ message: `Invalid data to encrypt/decrypt (is "${data}")`
67
+ });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Generates a deterministic IV based on the input data and key
73
+ * Uses MD5 for better stability across different CryptoJS versions
74
+ *
75
+ * @important DETERMINISTIC: Same data + key will ALWAYS produce the same IV
76
+ * across all systems and CryptoJS versions for consistent encryption results
77
+ */
78
+ private static generateIV(data: string, key: string, config: Required<CryptoConfig>): CryptoJS.lib.WordArray {
79
+ // Use MD5 for better stability - simpler hash algorithm, more consistent across versions
80
+ const combined = data + key;
81
+ const hash = CryptoJS.MD5(combined);
82
+ // Use slice(0, 4) for 16 bytes - more stable than division operations
83
+ return CryptoJS.lib.WordArray.create(hash.words.slice(0, 4));
84
+ }
85
+
86
+ /**
87
+ * Generates a deterministic salt based on the input data and key
88
+ * Uses MD5 for better stability across different CryptoJS versions
89
+ *
90
+ * @important DETERMINISTIC: Same data + key will ALWAYS produce the same salt
91
+ * across all systems and CryptoJS versions for consistent encryption results
92
+ */
93
+ private static generateSalt(data: string, key: string, config: Required<CryptoConfig>): CryptoJS.lib.WordArray {
94
+ // Use MD5 for better stability - simpler hash algorithm, more consistent across versions
95
+ const combined = key + data;
96
+ const hash = CryptoJS.MD5(combined);
97
+ // Use slice(0, 4) for 16 bytes - more stable than division operations
98
+ return CryptoJS.lib.WordArray.create(hash.words.slice(0, 4));
99
+ }
100
+
101
+ /**
102
+ * Derives a key using PBKDF2 with reduced iterations for stability
103
+ */
104
+ private static deriveKey(key: string, salt: CryptoJS.lib.WordArray, config: Required<CryptoConfig>): CryptoJS.lib.WordArray {
105
+ return CryptoJS.PBKDF2(key, salt, {
106
+ keySize: config.keySize,
107
+ iterations: config.keyIterations
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Safely serializes data to JSON
113
+ */
114
+ private static safeSerialize<T>(data: T): string {
115
+ try {
116
+ // Always use JSON.stringify to ensure proper serialization/deserialization
117
+ return JSON.stringify(data);
118
+ } catch (error) {
119
+ throw new DyFM_Error({
120
+ ...this.getDefaultErrorSettings('safeSerialize', error),
121
+ errorCode: 'DyFM-CRY-SER',
122
+ message: 'Failed to serialize data'
123
+ });
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Safely deserializes JSON data
129
+ */
130
+ private static safeDeserialize<T>(data: string): T {
131
+ try {
132
+ //let parsed = JSON.parse(data);
133
+ let parsed = DyFM_Object.failableSafeParseJSON(data);
134
+
135
+ // Handle double-stringified JSON (or more levels of stringification)
136
+ let maxAttempts = 3; // Prevent infinite loops
137
+ while (typeof parsed === 'string' && maxAttempts > 0) {
138
+ try {
139
+ //const nextParsed = JSON.parse(parsed);
140
+ const nextParsed = DyFM_Object.failableSafeParseJSON(parsed);
141
+ // Only continue if parsing actually changed the result
142
+ if (nextParsed !== parsed) {
143
+ parsed = nextParsed;
144
+ maxAttempts--;
145
+ } else {
146
+ break;
147
+ }
148
+ } catch {
149
+ // If parse fails, return current state
150
+ break;
151
+ }
152
+ }
153
+
154
+ // Handle primitive values
155
+ /* if (typeof parsed === 'string' || typeof parsed === 'number' || typeof parsed === 'boolean') {
156
+ return parsed as T;
157
+ } */
158
+
159
+ return parsed as T;
160
+ } catch (error) {
161
+ throw new DyFM_Error({
162
+ ...this.getDefaultErrorSettings('safeDeserialize', error),
163
+ errorCode: 'DyFM-CRY-DES',
164
+ message: 'Failed to deserialize data'
165
+ });
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Encrypts data using AES-256-CBC with deterministic IV and salt
171
+ *
172
+ * @important DETERMINISTIC BEHAVIOR: This method will produce identical encrypted
173
+ * output for identical input parameters across different systems, Node.js versions,
174
+ * and multiple function calls. The same data + key combination will ALWAYS generate
175
+ * the same encrypted string.
176
+ *
177
+ * @param data The data to encrypt
178
+ * @param key The encryption key
179
+ * @param config Optional configuration
180
+ * @returns URL-safe encrypted string that is identical across systems for same input
181
+ * @throws {DyFM_Error} if encryption fails
182
+ *
183
+ * @example
184
+ * // These will produce identical results on any system:
185
+ * const result1 = DyFM_Crypto.encrypt({id: 1}, "mykey");
186
+ * const result2 = DyFM_Crypto.encrypt({id: 1}, "mykey");
187
+ * console.log(result1 === result2); // Always true
188
+ */
189
+ static encrypt<T>(data: T, key: string, config?: CryptoConfig): string {
190
+ try {
191
+ this.validateInput(data, key);
192
+ const finalConfig = { ...this.DEFAULT_CONFIG, ...config };
193
+
194
+ // Convert data to string
195
+ const dataStr = this.safeSerialize(data);
196
+
197
+ // Generate deterministic IV and salt based on data and key
198
+ const iv = this.generateIV(dataStr, key, finalConfig);
199
+ const salt = this.generateSalt(dataStr, key, finalConfig);
200
+
201
+ // Derive key using PBKDF2
202
+ const derivedKey = this.deriveKey(key, salt, finalConfig);
203
+
204
+ // Encrypt the data
205
+ const encrypted = CryptoJS.AES.encrypt(dataStr, derivedKey, {
206
+ iv: iv,
207
+ mode: CryptoJS.mode.CBC,
208
+ padding: CryptoJS.pad.Pkcs7
209
+ });
210
+
211
+ // Combine IV + Salt + Ciphertext
212
+ const combined = iv.concat(salt).concat(encrypted.ciphertext);
213
+
214
+ // Convert to URL-safe base64
215
+ return CryptoJS.enc.Base64.stringify(combined)
216
+ .replace(/\+/g, '-')
217
+ .replace(/\//g, '_')
218
+ .replace(/=+$/, '');
219
+ } catch (error) {
220
+ throw new DyFM_Error({
221
+ ...this.getDefaultErrorSettings('encrypt', error),
222
+ errorCode: 'DyFM-CRY-ENC',
223
+ });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Decrypts data that was encrypted using encrypt()
229
+ * @param encryptedData The encrypted data
230
+ * @param key The decryption key
231
+ * @param config Optional configuration
232
+ * @returns The decrypted data
233
+ * @throws {DyFM_Error} if decryption fails
234
+ */
235
+ static decrypt<T>(encryptedData: string, key: string, config?: CryptoConfig): T {
236
+ try {
237
+ this.validateInput(encryptedData, key);
238
+ const finalConfig = { ...this.DEFAULT_CONFIG, ...config };
239
+
240
+ // Convert from URL-safe base64
241
+ const base64 = encryptedData
242
+ .replace(/-/g, '+')
243
+ .replace(/_/g, '/');
244
+
245
+ // Parse the combined data
246
+ const combined = CryptoJS.enc.Base64.parse(base64);
247
+
248
+ // Validate minimum length (IV + Salt + minimum ciphertext)
249
+ const minLength = (finalConfig.ivLength + finalConfig.saltLength + 16) / 4; // 16 bytes minimum for ciphertext
250
+ if (combined.words.length < minLength) {
251
+ throw new Error('Invalid encrypted data length');
252
+ }
253
+
254
+ // Extract IV, salt, and ciphertext
255
+ const iv = CryptoJS.lib.WordArray.create(combined.words.slice(0, finalConfig.ivLength / 4));
256
+ const salt = CryptoJS.lib.WordArray.create(
257
+ combined.words.slice(
258
+ finalConfig.ivLength / 4,
259
+ (finalConfig.ivLength + finalConfig.saltLength) / 4
260
+ )
261
+ );
262
+ const ciphertext = CryptoJS.lib.WordArray.create(
263
+ combined.words.slice((finalConfig.ivLength + finalConfig.saltLength) / 4)
264
+ );
265
+
266
+ // Derive key using PBKDF2
267
+ const derivedKey = this.deriveKey(key, salt, finalConfig);
268
+
269
+ // Decrypt the data
270
+ const decrypted = CryptoJS.AES.decrypt(
271
+ { ciphertext: ciphertext },
272
+ derivedKey,
273
+ {
274
+ iv: iv,
275
+ mode: CryptoJS.mode.CBC,
276
+ padding: CryptoJS.pad.Pkcs7
277
+ }
278
+ );
279
+
280
+ // Parse JSON
281
+ const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
282
+ return this.safeDeserialize<T>(decryptedStr);
283
+ } catch (error) {
284
+ throw new DyFM_Error({
285
+ ...this.getDefaultErrorSettings('decrypt', error),
286
+ errorCode: 'DyFM-CRY-DRY',
287
+ });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Generates a secure random key
293
+ * @param length Length of the key in bytes (default: 32)
294
+ * @returns A secure random key
295
+ */
296
+ static generateKey(length: number = 32): string {
297
+ return CryptoJS.lib.WordArray.random(length).toString();
298
+ }
299
+
300
+ /**
301
+ * Validates if a string is a valid encrypted data
302
+ * @param encryptedData The data to validate
303
+ * @returns true if the data appears to be valid encrypted data
304
+ */
305
+ static isValidEncryptedData(encryptedData: string): boolean {
306
+ if (!encryptedData || typeof encryptedData !== 'string') {
307
+ return false;
308
+ }
309
+ return /^[A-Za-z0-9\-_]+$/.test(encryptedData);
310
+ }
311
+
312
+ /**
313
+ * Gets default error settings
314
+ */
315
+ private static getDefaultErrorSettings(operation: string, error?: any): DyFM_Error_Settings {
316
+ return {
317
+ status: (error as DyFM_Error)?.___status ?? (error as any)?.status ?? 401,
318
+ message: `Crypto operation "${operation}" failed`,
319
+ error: error,
320
+ errorCode: 'DyFM-CRY-ERR'
321
+ };
322
+ }
323
+ }