@20syldev/api 4.6.0 → 4.8.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.
Files changed (111) hide show
  1. package/README.md +5 -3
  2. package/dist/app.js +6 -6
  3. package/dist/app.js.map +1 -1
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/config/versions.js +22 -3
  6. package/dist/config/versions.js.map +1 -1
  7. package/dist/constants.js +27 -12
  8. package/dist/constants.js.map +1 -1
  9. package/dist/middleware/error.js +2 -2
  10. package/dist/middleware/error.js.map +1 -1
  11. package/dist/middleware/logger.js.map +1 -1
  12. package/dist/middleware/ratelimit.js +2 -2
  13. package/dist/middleware/ratelimit.js.map +1 -1
  14. package/dist/middleware/version.js.map +1 -1
  15. package/dist/modules/v3.js +1 -1
  16. package/dist/modules/v3.js.map +1 -1
  17. package/dist/modules/v4/address.js +381 -0
  18. package/dist/modules/v4/address.js.map +1 -0
  19. package/dist/modules/v4/agent.js +92 -0
  20. package/dist/modules/v4/agent.js.map +1 -0
  21. package/dist/modules/v4/algorithms.js +79 -0
  22. package/dist/modules/v4/algorithms.js.map +1 -1
  23. package/dist/modules/v4/avatar.js +136 -0
  24. package/dist/modules/v4/avatar.js.map +1 -0
  25. package/dist/modules/v4/barcode.js +440 -0
  26. package/dist/modules/v4/barcode.js.map +1 -0
  27. package/dist/modules/v4/captcha.js +7 -0
  28. package/dist/modules/v4/captcha.js.map +1 -1
  29. package/dist/modules/v4/chat.js +9 -1
  30. package/dist/modules/v4/chat.js.map +1 -1
  31. package/dist/modules/v4/color.js +8 -1
  32. package/dist/modules/v4/color.js.map +1 -1
  33. package/dist/modules/v4/convert.js +9 -0
  34. package/dist/modules/v4/convert.js.map +1 -1
  35. package/dist/modules/v4/credit.js +104 -0
  36. package/dist/modules/v4/credit.js.map +1 -0
  37. package/dist/modules/v4/cron.js +202 -0
  38. package/dist/modules/v4/cron.js.map +1 -0
  39. package/dist/modules/v4/dice.js +7 -0
  40. package/dist/modules/v4/dice.js.map +1 -1
  41. package/dist/modules/v4/domain.js +6 -1
  42. package/dist/modules/v4/domain.js.map +1 -1
  43. package/dist/modules/v4/encode.js +71 -0
  44. package/dist/modules/v4/encode.js.map +1 -1
  45. package/dist/modules/v4/geo.js +11 -1
  46. package/dist/modules/v4/geo.js.map +1 -1
  47. package/dist/modules/v4/hash.js +10 -1
  48. package/dist/modules/v4/hash.js.map +1 -1
  49. package/dist/modules/v4/hyperplanning.js +9 -1
  50. package/dist/modules/v4/hyperplanning.js.map +1 -1
  51. package/dist/modules/v4/ip.js +132 -0
  52. package/dist/modules/v4/ip.js.map +1 -0
  53. package/dist/modules/v4/levenshtein.js +8 -0
  54. package/dist/modules/v4/levenshtein.js.map +1 -1
  55. package/dist/modules/v4/palette.js +9 -1
  56. package/dist/modules/v4/palette.js.map +1 -1
  57. package/dist/modules/v4/password.js +343 -0
  58. package/dist/modules/v4/password.js.map +1 -0
  59. package/dist/modules/v4/personal.js +14 -18
  60. package/dist/modules/v4/personal.js.map +1 -1
  61. package/dist/modules/v4/placeholder.js +8 -0
  62. package/dist/modules/v4/placeholder.js.map +1 -1
  63. package/dist/modules/v4/qrcode.js +10 -3
  64. package/dist/modules/v4/qrcode.js.map +1 -1
  65. package/dist/modules/v4/regex.js +48 -0
  66. package/dist/modules/v4/regex.js.map +1 -0
  67. package/dist/modules/v4/statistics.js +7 -0
  68. package/dist/modules/v4/statistics.js.map +1 -1
  69. package/dist/modules/v4/text.js +30 -0
  70. package/dist/modules/v4/text.js.map +1 -1
  71. package/dist/modules/v4/tic_tac_toe.js +9 -1
  72. package/dist/modules/v4/tic_tac_toe.js.map +1 -1
  73. package/dist/modules/v4/time.js +62 -11
  74. package/dist/modules/v4/time.js.map +1 -1
  75. package/dist/modules/v4/token.js +17 -8
  76. package/dist/modules/v4/token.js.map +1 -1
  77. package/dist/modules/v4/username.js +5 -0
  78. package/dist/modules/v4/username.js.map +1 -1
  79. package/dist/modules/v4/validate.js +21 -0
  80. package/dist/modules/v4/validate.js.map +1 -1
  81. package/dist/modules/v4.js +10 -1
  82. package/dist/modules/v4.js.map +1 -1
  83. package/dist/routes/delete.js.map +1 -1
  84. package/dist/routes/get.js +208 -48
  85. package/dist/routes/get.js.map +1 -1
  86. package/dist/routes/index.js +2 -2
  87. package/dist/routes/index.js.map +1 -1
  88. package/dist/routes/patch.js.map +1 -1
  89. package/dist/routes/post.js +2 -2
  90. package/dist/routes/post.js.map +1 -1
  91. package/dist/utils/response.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/config/versions.ts +8 -1
  94. package/src/constants.ts +27 -12
  95. package/src/modules/v4/address.ts +412 -0
  96. package/src/modules/v4/cron.ts +221 -0
  97. package/src/modules/v4/geo.ts +2 -1
  98. package/src/modules/v4/password.ts +379 -0
  99. package/src/modules/v4/personal.ts +4 -16
  100. package/src/modules/v4/qrcode.ts +2 -2
  101. package/src/modules/v4/regex.ts +67 -0
  102. package/src/modules/v4/time.ts +63 -17
  103. package/src/modules/v4/token.ts +8 -7
  104. package/src/modules/v4.ts +4 -0
  105. package/src/routes/get.ts +125 -46
  106. package/tests/integration/api.test.ts +111 -0
  107. package/tests/unit/address.test.ts +84 -0
  108. package/tests/unit/cron.test.ts +110 -0
  109. package/tests/unit/password.test.ts +86 -0
  110. package/tests/unit/regex.test.ts +95 -0
  111. package/tests/unit/time.test.ts +47 -0
@@ -0,0 +1,379 @@
1
+ import { randomInt } from 'crypto';
2
+
3
+ import { MAX_PASSWORD_COUNT, MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '../../constants.js';
4
+
5
+ const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
6
+ const LOWER = 'abcdefghijklmnopqrstuvwxyz';
7
+ const DIGITS = '0123456789';
8
+ const SYMBOLS = '!@#$%^&*()-_=+[]{}|;:,.<>?';
9
+
10
+ const WORDLIST = [
11
+ 'apple',
12
+ 'brave',
13
+ 'chair',
14
+ 'dance',
15
+ 'eagle',
16
+ 'fancy',
17
+ 'grace',
18
+ 'house',
19
+ 'ivory',
20
+ 'joker',
21
+ 'kneel',
22
+ 'lemon',
23
+ 'maple',
24
+ 'noble',
25
+ 'ocean',
26
+ 'piano',
27
+ 'queen',
28
+ 'river',
29
+ 'stone',
30
+ 'tiger',
31
+ 'ultra',
32
+ 'vivid',
33
+ 'waste',
34
+ 'xenon',
35
+ 'yacht',
36
+ 'zebra',
37
+ 'amber',
38
+ 'blaze',
39
+ 'coral',
40
+ 'drift',
41
+ 'ember',
42
+ 'flora',
43
+ 'globe',
44
+ 'haven',
45
+ 'input',
46
+ 'jumpy',
47
+ 'karma',
48
+ 'lunar',
49
+ 'magic',
50
+ 'night',
51
+ 'optic',
52
+ 'proxy',
53
+ 'quirk',
54
+ 'radar',
55
+ 'solar',
56
+ 'trace',
57
+ 'umbra',
58
+ 'vapor',
59
+ 'witch',
60
+ 'xylem',
61
+ 'yield',
62
+ 'zippy',
63
+ 'alpha',
64
+ 'brisk',
65
+ 'crane',
66
+ 'depth',
67
+ 'event',
68
+ 'frost',
69
+ 'grime',
70
+ 'harsh',
71
+ 'index',
72
+ 'joint',
73
+ 'knack',
74
+ 'level',
75
+ 'minus',
76
+ 'nerve',
77
+ 'onset',
78
+ 'phase',
79
+ 'quest',
80
+ 'ratio',
81
+ 'scope',
82
+ 'trend',
83
+ 'union',
84
+ 'valve',
85
+ 'weary',
86
+ 'young',
87
+ 'blunt',
88
+ 'clean',
89
+ 'drive',
90
+ 'earth',
91
+ 'fiber',
92
+ 'grain',
93
+ 'grind',
94
+ 'horse',
95
+ 'ideal',
96
+ 'issue',
97
+ 'judge',
98
+ 'juice',
99
+ 'knave',
100
+ 'large',
101
+ 'laser',
102
+ 'light',
103
+ 'limit',
104
+ 'media',
105
+ 'merit',
106
+ 'metal',
107
+ 'model',
108
+ 'money',
109
+ 'month',
110
+ 'motor',
111
+ 'mount',
112
+ 'music',
113
+ 'nerve',
114
+ 'noise',
115
+ 'north',
116
+ 'noted',
117
+ 'novel',
118
+ 'nurse',
119
+ 'oasis',
120
+ 'offer',
121
+ 'olive',
122
+ 'omega',
123
+ 'other',
124
+ 'outer',
125
+ 'owner',
126
+ 'oxide',
127
+ 'ozone',
128
+ 'paint',
129
+ 'panel',
130
+ 'paper',
131
+ 'patch',
132
+ 'pause',
133
+ 'peace',
134
+ 'pearl',
135
+ 'pedal',
136
+ 'plain',
137
+ 'plank',
138
+ 'plant',
139
+ 'plate',
140
+ 'plaza',
141
+ 'pluck',
142
+ 'plumb',
143
+ 'plume',
144
+ 'plunge',
145
+ 'point',
146
+ 'polar',
147
+ 'power',
148
+ 'press',
149
+ 'pride',
150
+ 'prime',
151
+ 'print',
152
+ 'prism',
153
+ 'prize',
154
+ 'probe',
155
+ 'prone',
156
+ 'proof',
157
+ 'prose',
158
+ 'proud',
159
+ 'prove',
160
+ 'prowl',
161
+ 'pulse',
162
+ 'punch',
163
+ 'pupil',
164
+ 'purge',
165
+ 'swift',
166
+ 'sword',
167
+ 'table',
168
+ 'taint',
169
+ 'tally',
170
+ 'taste',
171
+ 'teach',
172
+ 'teeth',
173
+ 'tempo',
174
+ 'tense',
175
+ 'tenth',
176
+ 'terms',
177
+ 'terra',
178
+ 'thick',
179
+ 'thing',
180
+ 'think',
181
+ 'thorn',
182
+ 'three',
183
+ 'throw',
184
+ 'thumb',
185
+ 'tidal',
186
+ 'tight',
187
+ 'tilde',
188
+ 'timer',
189
+ 'title',
190
+ 'token',
191
+ 'torch',
192
+ 'total',
193
+ 'touch',
194
+ 'tough',
195
+ 'tower',
196
+ 'track',
197
+ 'trade',
198
+ 'trail',
199
+ 'train',
200
+ 'trait',
201
+ 'tramp',
202
+ 'tread',
203
+ 'treat',
204
+ 'trees',
205
+ 'trial',
206
+ 'tribe',
207
+ 'trick',
208
+ 'tried',
209
+ 'troop',
210
+ 'trove',
211
+ 'truce',
212
+ 'truck',
213
+ 'truly',
214
+ 'trunk',
215
+ 'truth',
216
+ 'tulip',
217
+ 'tumor',
218
+ 'tuner',
219
+ 'tunic',
220
+ 'tweak',
221
+ 'twice',
222
+ 'twirl',
223
+ 'twist',
224
+ 'typed',
225
+ 'under',
226
+ 'unify',
227
+ 'unite',
228
+ 'unity',
229
+ 'until',
230
+ 'upper',
231
+ 'upset',
232
+ 'urban',
233
+ 'usage',
234
+ 'valid',
235
+ 'value',
236
+ 'visor',
237
+ 'vital',
238
+ 'vivid',
239
+ 'vocab',
240
+ 'vocal',
241
+ 'voice',
242
+ 'voter',
243
+ 'vault',
244
+ 'venue',
245
+ 'verse',
246
+ 'watch',
247
+ 'water',
248
+ 'weave',
249
+ 'wedge',
250
+ 'weird',
251
+ 'wheel',
252
+ 'where',
253
+ 'which',
254
+ 'while',
255
+ 'white',
256
+ 'whole',
257
+ 'wider',
258
+ 'windy',
259
+ 'wired',
260
+ 'world',
261
+ 'worse',
262
+ 'worth',
263
+ 'would',
264
+ 'wound',
265
+ 'write',
266
+ 'wrong',
267
+ 'wrote',
268
+ 'years',
269
+ 'yield',
270
+ 'yours',
271
+ ];
272
+
273
+ export interface PasswordResult {
274
+ passwords: string[];
275
+ type: string;
276
+ length: number;
277
+ strength: string;
278
+ entropy: number;
279
+ }
280
+
281
+ function computeStrength(entropy: number): string {
282
+ if (entropy < 40) return 'weak';
283
+ if (entropy < 60) return 'moderate';
284
+ if (entropy < 80) return 'strong';
285
+ return 'very_strong';
286
+ }
287
+
288
+ function generateRandom(length: number, charset: string): string {
289
+ return Array.from({ length }, () => charset[randomInt(charset.length)]).join('');
290
+ }
291
+
292
+ function generatePassphrase(wordCount: number, separator: string): string {
293
+ return Array.from({ length: wordCount }, () => WORDLIST[randomInt(WORDLIST.length)]).join(separator);
294
+ }
295
+
296
+ /**
297
+ * Generates one or more passwords using random characters or passphrase mode.
298
+ *
299
+ * @param type - Generation mode: "random" or "passphrase"
300
+ * @param length - Password length for random mode (8–128), or word count for passphrase mode (3–10)
301
+ * @param options - Character set options: uppercase, lowercase, digits, symbols, exclude, count, separator
302
+ * @returns Generated passwords with type, length, strength and entropy
303
+ * @throws Error if no charset is active, length is out of range, or count exceeds the maximum
304
+ */
305
+ export default function password(
306
+ type: string = 'random',
307
+ length: number = 16,
308
+ options: {
309
+ uppercase?: boolean;
310
+ lowercase?: boolean;
311
+ digits?: boolean;
312
+ symbols?: boolean;
313
+ exclude?: string;
314
+ count?: number;
315
+ separator?: string;
316
+ } = {},
317
+ ): PasswordResult {
318
+ const {
319
+ uppercase = true,
320
+ lowercase = true,
321
+ digits = true,
322
+ symbols = false,
323
+ exclude = '',
324
+ count = 1,
325
+ separator = '-',
326
+ } = options;
327
+
328
+ if (count < 1 || count > MAX_PASSWORD_COUNT) {
329
+ throw new Error(`Count must be between 1 and ${MAX_PASSWORD_COUNT}`);
330
+ }
331
+
332
+ if (type === 'passphrase') {
333
+ const wordCount = Math.max(3, Math.min(10, length));
334
+ const entropy = Math.log2(Math.pow(WORDLIST.length, wordCount));
335
+ const passwords = Array.from({ length: count }, () => generatePassphrase(wordCount, separator));
336
+ return {
337
+ passwords,
338
+ type: 'passphrase',
339
+ length: wordCount,
340
+ strength: computeStrength(entropy),
341
+ entropy: Math.round(entropy * 10) / 10,
342
+ };
343
+ }
344
+
345
+ if (type !== 'random') {
346
+ throw new Error('Type must be "random" or "passphrase"');
347
+ }
348
+
349
+ if (isNaN(length) || length < MIN_PASSWORD_LENGTH || length > MAX_PASSWORD_LENGTH) {
350
+ throw new Error(`Length must be between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH}`);
351
+ }
352
+
353
+ let charset = '';
354
+ if (uppercase) charset += UPPER;
355
+ if (lowercase) charset += LOWER;
356
+ if (digits) charset += DIGITS;
357
+ if (symbols) charset += SYMBOLS;
358
+
359
+ if (exclude) {
360
+ for (const char of exclude) {
361
+ charset = charset.replaceAll(char, '');
362
+ }
363
+ }
364
+
365
+ if (charset.length === 0) {
366
+ throw new Error('At least one character set must be enabled');
367
+ }
368
+
369
+ const entropy = Math.log2(Math.pow(charset.length, length));
370
+ const passwords = Array.from({ length: count }, () => generateRandom(length, charset));
371
+
372
+ return {
373
+ passwords,
374
+ type: 'random',
375
+ length,
376
+ strength: computeStrength(entropy),
377
+ entropy: Math.round(entropy * 10) / 10,
378
+ };
379
+ }
@@ -1,4 +1,5 @@
1
1
  import { random, randomNumber } from '../../utils/helpers.js';
2
+ import addressFn from './address.js';
2
3
 
3
4
  interface Person {
4
5
  name: string;
@@ -47,21 +48,6 @@ export default function personal(): Record<string, unknown> {
47
48
 
48
49
  const jobs = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Doctor'];
49
50
  const hobbies = ['Reading', 'Traveling', 'Gaming', 'Cooking', 'Fitness', 'Music', 'Photography', 'Writing'];
50
- const cities = [
51
- 'New York',
52
- 'Paris',
53
- 'London',
54
- 'Madrid',
55
- 'Berlin',
56
- 'Rome',
57
- 'Tokyo',
58
- 'Los Angeles',
59
- 'Sydney',
60
- 'São Paulo',
61
- 'Toronto',
62
- ];
63
- const streets = ['Main St', '2nd Ave', 'Broadway', 'Park Lane', 'Elm St', 'Sunset Blvd', 'Maple St', 'Highland Rd'];
64
-
65
51
  const card = Array.from({ length: 4 }, () => randomNumber(1000, 9999)).join(' ');
66
52
  const cvc = randomNumber(100, 999);
67
53
  const expiration = `${String(randomNumber(1, 12)).padStart(2, '0')}/${(new Date().getFullYear() + randomNumber(0, 3)).toString().slice(-2)}`;
@@ -69,6 +55,8 @@ export default function personal(): Record<string, unknown> {
69
55
  const person = random(people);
70
56
  const social = person.social;
71
57
  const country = person.country;
58
+ const countryCodes: Record<string, string> = { US: 'us', FR: 'fr', UK: 'uk', ES: 'es', DE: 'de' };
59
+ const addrCountry = countryCodes[country] ?? 'us';
72
60
  const countryInfo = countries[country]!;
73
61
  const phone = countryInfo.tel;
74
62
  const lang = countryInfo.lang;
@@ -129,7 +117,7 @@ export default function personal(): Record<string, unknown> {
129
117
  card,
130
118
  cvc,
131
119
  expiration,
132
- address: `${randomNumber(1, 9999)} ${random(streets)}, ${random(cities)}`,
120
+ address: addressFn(addrCountry).addresses[0]!,
133
121
  birthday,
134
122
  civil_status: civilStatus,
135
123
  children,
@@ -1,6 +1,7 @@
1
1
  import { createCanvas, loadImage } from 'canvas';
2
2
  import { toBuffer, toCanvas as qrToCanvas, toDataURL } from 'qrcode';
3
3
 
4
+ import { MAX_QRCODE_LOGO_BYTES } from '../../constants.js';
4
5
  import { normalizeColor } from '../../utils/colors.js';
5
6
 
6
7
  export interface QRCodeOptions {
@@ -24,7 +25,6 @@ export interface QRCodeResult {
24
25
 
25
26
  const CORRECTIONS = new Set(['L', 'M', 'Q', 'H']);
26
27
  const FORMATS = new Set(['png', 'base64']);
27
- const MAX_LOGO_BYTES = 2 * 1024 * 1024;
28
28
 
29
29
  function clamp(value: number | undefined, name: string, def: number, min: number, max: number): number {
30
30
  if (value === undefined) return def;
@@ -44,7 +44,7 @@ async function fetchIcon(url: string): Promise<Buffer> {
44
44
  if (!contentType.startsWith('image/')) throw new Error('Icon URL must point to an image');
45
45
 
46
46
  const buffer = Buffer.from(await response.arrayBuffer());
47
- if (buffer.length > MAX_LOGO_BYTES) throw new Error('Icon must be under 2MB');
47
+ if (buffer.length > MAX_QRCODE_LOGO_BYTES) throw new Error('Icon must be under 2MB');
48
48
 
49
49
  return buffer;
50
50
  }
@@ -0,0 +1,67 @@
1
+ import { MAX_PATTERN_LENGTH, MAX_REGEX_MATCHES, MAX_STRING_LENGTH } from '../../constants.js';
2
+
3
+ const VALID_FLAGS = new Set(['g', 'i', 'm', 's', 'u']);
4
+
5
+ export interface RegexMatch {
6
+ match: string;
7
+ index: number;
8
+ groups: string[];
9
+ namedGroups: Record<string, string>;
10
+ }
11
+
12
+ export interface RegexResult {
13
+ valid: boolean;
14
+ pattern: string;
15
+ flags: string;
16
+ matches: RegexMatch[];
17
+ count: number;
18
+ }
19
+
20
+ /**
21
+ * Tests a regular expression pattern against a text and returns structured match results.
22
+ *
23
+ * @param pattern - The regex pattern to test (max 200 characters)
24
+ * @param text - The text to match against (max 1000 characters)
25
+ * @param flags - Optional regex flags; only g, i, m, s, u are accepted (g is always forced)
26
+ * @returns Match results with groups and named groups, or { valid: false } if the pattern is invalid
27
+ * @throws Error if pattern or text is missing or exceeds the maximum length
28
+ */
29
+ export default function regex(pattern: string, text: string, flags: string = 'g'): RegexResult {
30
+ if (!pattern) {
31
+ throw new Error('Please provide a pattern (?pattern={regex})');
32
+ }
33
+ if (!text) {
34
+ throw new Error('Please provide a text (&text={string})');
35
+ }
36
+ if (pattern.length > MAX_PATTERN_LENGTH) {
37
+ throw new Error(`Pattern must be under ${MAX_PATTERN_LENGTH} characters`);
38
+ }
39
+ if (text.length > MAX_STRING_LENGTH) {
40
+ throw new Error(`Text must be under ${MAX_STRING_LENGTH} characters`);
41
+ }
42
+
43
+ const cleanedFlags = [...new Set(['g', ...flags.split('').filter((f) => VALID_FLAGS.has(f))])].join('');
44
+
45
+ let re: RegExp;
46
+ try {
47
+ re = new RegExp(pattern, cleanedFlags);
48
+ } catch {
49
+ return { valid: false, pattern, flags: cleanedFlags, matches: [], count: 0 };
50
+ }
51
+
52
+ const matches: RegexMatch[] = [];
53
+ let result: RegExpExecArray | null;
54
+
55
+ while ((result = re.exec(text)) !== null && matches.length < MAX_REGEX_MATCHES) {
56
+ matches.push({
57
+ match: result[0],
58
+ index: result.index,
59
+ groups: result.slice(1).map((g) => g ?? ''),
60
+ namedGroups: result.groups ? { ...result.groups } : {},
61
+ });
62
+ // Prevent infinite loop on zero-length matches
63
+ if (result[0].length === 0) re.lastIndex++;
64
+ }
65
+
66
+ return { valid: true, pattern, flags: cleanedFlags, matches, count: matches.length };
67
+ }
@@ -1,4 +1,6 @@
1
- const validFormats = [
1
+ import { MAX_COUNTDOWN_YEARS } from '../../constants.js';
2
+
3
+ const VALID_FORMATS = [
2
4
  'iso',
3
5
  'utc',
4
6
  'timestamp',
@@ -19,21 +21,23 @@ const validFormats = [
19
21
  'timezoneOffset',
20
22
  ] as const;
21
23
 
22
- const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'] as const;
24
+ export const VALID_TIMEZONES = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'] as const;
23
25
 
24
- type TimeFormat = (typeof validFormats)[number];
25
- type Timezone = (typeof validTimezones)[number];
26
+ type TimeFormat = (typeof VALID_FORMATS)[number];
27
+ type Timezone = (typeof VALID_TIMEZONES)[number];
26
28
 
27
29
  /**
28
- * Returns the current or a random date/time in various formats and timezones.
30
+ * Returns the current or a random date/time in various formats and timezones,
31
+ * or computes a countdown/elapsed time to/from a target date.
29
32
  *
30
- * @param type - "live" for the current time or "random" for a random date within a range
33
+ * @param type - "live" for the current time, "random" for a random date, or "countdown" for a time diff
31
34
  * @param start - Optional start date for random mode (YYYY-MM-DD)
32
35
  * @param end - Optional end date for random mode (YYYY-MM-DD)
33
36
  * @param format - Optional specific format to return (e.g. "iso", "timestamp", "year")
34
37
  * @param timezone - Optional timezone (e.g. "UTC", "Europe/Paris")
35
- * @returns Object containing all time formats, or a single format if specified
36
- * @throws Error if type, format, or timezone is invalid
38
+ * @param target - Required for countdown mode: target date in ISO 8601 or YYYY-MM-DD format
39
+ * @returns Object containing all time formats, a single format, or a countdown result
40
+ * @throws Error if type, format, timezone, or target is invalid
37
41
  */
38
42
  export default function time(
39
43
  type: string = 'live',
@@ -41,9 +45,55 @@ export default function time(
41
45
  end?: string,
42
46
  format?: string,
43
47
  timezone?: string,
48
+ target?: string,
44
49
  ): Record<string, unknown> {
45
- if (type !== 'live' && type !== 'random') {
46
- throw new Error('Please provide a valid type (live or random)');
50
+ if (type !== 'live' && type !== 'random' && type !== 'countdown') {
51
+ throw new Error('Please provide a valid type (live, random or countdown)');
52
+ }
53
+
54
+ if (timezone && !VALID_TIMEZONES.includes(timezone as Timezone)) {
55
+ throw new Error(`Please provide a valid timezone. Options: ${VALID_TIMEZONES.join(', ')}`);
56
+ }
57
+
58
+ if (type === 'countdown') {
59
+ if (!target) {
60
+ throw new Error('Please provide a target date (?target=YYYY-MM-DD)');
61
+ }
62
+
63
+ const targetDate = new Date(target);
64
+ if (isNaN(targetDate.getTime())) {
65
+ throw new Error('Please provide a valid target date (ISO 8601 or YYYY-MM-DD)');
66
+ }
67
+
68
+ const now = new Date();
69
+ const maxMs = MAX_COUNTDOWN_YEARS * 365.25 * 24 * 3600 * 1000;
70
+
71
+ if (Math.abs(targetDate.getTime() - now.getTime()) > maxMs) {
72
+ throw new Error(`Target date must be within ${MAX_COUNTDOWN_YEARS} years from now`);
73
+ }
74
+
75
+ const direction: 'future' | 'past' = targetDate > now ? 'future' : 'past';
76
+ const diffMs = Math.abs(targetDate.getTime() - now.getTime());
77
+ const total_seconds = Math.floor(diffMs / 1000);
78
+
79
+ const days = Math.floor(total_seconds / 86400);
80
+ const hours = Math.floor((total_seconds % 86400) / 3600);
81
+ const minutes = Math.floor((total_seconds % 3600) / 60);
82
+ const seconds = total_seconds % 60;
83
+
84
+ const parts: string[] = [];
85
+ if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`);
86
+ if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? 's' : ''}`);
87
+ if (minutes > 0) parts.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`);
88
+ if (seconds > 0) parts.push(`${seconds} second${seconds !== 1 ? 's' : ''}`);
89
+
90
+ return {
91
+ target: targetDate.toISOString(),
92
+ now: now.toISOString(),
93
+ direction,
94
+ remaining: { total_seconds, days, hours, minutes, seconds },
95
+ human: parts.length > 0 ? parts.join(', ') : '0 seconds',
96
+ };
47
97
  }
48
98
 
49
99
  if (start && !Date.parse(start)) {
@@ -54,12 +104,8 @@ export default function time(
54
104
  throw new Error('Please provide a valid end date (YYYY-MM-DD)');
55
105
  }
56
106
 
57
- if (format && !validFormats.includes(format as TimeFormat)) {
58
- throw new Error(`Please provide a valid format. Options: ${validFormats.join(', ')}`);
59
- }
60
-
61
- if (timezone && !validTimezones.includes(timezone as Timezone)) {
62
- throw new Error(`Please provide a valid timezone. Options: ${validTimezones.join(', ')}`);
107
+ if (format && !VALID_FORMATS.includes(format as TimeFormat)) {
108
+ throw new Error(`Please provide a valid format. Options: ${VALID_FORMATS.join(', ')}`);
63
109
  }
64
110
 
65
111
  const getTimeFormats = (date: Date, tz: string): Record<string, unknown> => {
@@ -89,7 +135,7 @@ export default function time(
89
135
  const startDate = start ? new Date(start).getTime() : new Date('1900-01-01').getTime();
90
136
  const endDate = end ? new Date(end).getTime() : new Date('2100-12-31').getTime();
91
137
  const randomDate = new Date(startDate + Math.random() * (endDate - startDate));
92
- const tz = timezone || validTimezones[Math.floor(Math.random() * validTimezones.length)]!;
138
+ const tz = timezone || VALID_TIMEZONES[Math.floor(Math.random() * VALID_TIMEZONES.length)]!;
93
139
  const formats = getTimeFormats(randomDate, tz);
94
140
 
95
141
  return format ? { date: formats[format] } : formats;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes, randomInt } from 'crypto';
2
2
  import { v4 } from 'uuid';
3
3
 
4
4
  import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../../constants.js';
@@ -20,11 +20,8 @@ export default function token(len: number, type: string = 'alphanum'): string {
20
20
  throw new Error('Length cannot exceed 4096');
21
21
  }
22
22
 
23
- const genToken = (chars: string, length: number): string => {
24
- return Array.from({ length }, () => {
25
- return chars[Math.floor(Math.random() * chars.length)];
26
- }).join('');
27
- };
23
+ const genToken = (chars: string, length: number): string =>
24
+ Array.from({ length }, () => chars[randomInt(chars.length)]).join('');
28
25
 
29
26
  const tokenTypes: Record<string, () => string> = {
30
27
  alpha: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
@@ -40,7 +37,11 @@ export default function token(len: number, type: string = 'alphanum'): string {
40
37
  num: () => genToken('0123456789', len),
41
38
  punct: () => genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
42
39
  urlsafe: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
43
- uuid: () => v4().replace(/-/g, '').slice(0, len),
40
+ uuid: () => {
41
+ let result = '';
42
+ while (result.length < len) result += v4().replace(/-/g, '');
43
+ return result.slice(0, len);
44
+ },
44
45
  };
45
46
 
46
47
  const lowerType = type.toLowerCase();
package/src/modules/v4.ts CHANGED
@@ -1,9 +1,11 @@
1
+ export { default as address } from './v4/address.js';
1
2
  export { default as agent } from './v4/agent.js';
2
3
  export * as algorithms from './v4/algorithms.js';
3
4
  export { default as captcha } from './v4/captcha.js';
4
5
  export { default as chat } from './v4/chat.js';
5
6
  export { default as color } from './v4/color.js';
6
7
  export { default as convert } from './v4/convert.js';
8
+ export { default as cron } from './v4/cron.js';
7
9
  export { default as dice } from './v4/dice.js';
8
10
  export { default as domain } from './v4/domain.js';
9
11
  export * as encode from './v4/encode.js';
@@ -13,9 +15,11 @@ export { default as hyperplanning } from './v4/hyperplanning.js';
13
15
  export { default as ip } from './v4/ip.js';
14
16
  export { default as levenshtein } from './v4/levenshtein.js';
15
17
  export { default as palette } from './v4/palette.js';
18
+ export { default as password } from './v4/password.js';
16
19
  export { default as personal } from './v4/personal.js';
17
20
  export { default as placeholder } from './v4/placeholder.js';
18
21
  export { default as qrcode } from './v4/qrcode.js';
22
+ export { default as regex } from './v4/regex.js';
19
23
  export { default as statistics } from './v4/statistics.js';
20
24
  export * as text from './v4/text.js';
21
25
  export { default as tic_tac_toe } from './v4/tic_tac_toe.js';