@20syldev/api 4.7.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 (107) hide show
  1. package/README.md +3 -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 +3 -1
  94. package/src/constants.ts +27 -12
  95. package/src/modules/v4/address.ts +6 -7
  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 +5 -7
  99. package/src/modules/v4/qrcode.ts +2 -2
  100. package/src/modules/v4/regex.ts +67 -0
  101. package/src/modules/v4/time.ts +63 -17
  102. package/src/modules/v4.ts +2 -0
  103. package/src/routes/get.ts +67 -46
  104. package/tests/integration/api.test.ts +56 -0
  105. package/tests/unit/cron.test.ts +110 -0
  106. package/tests/unit/regex.test.ts +95 -0
  107. package/tests/unit/time.test.ts +47 -0
@@ -331,6 +331,19 @@ describe('GET /v4/time', () => {
331
331
  assert.ok(ts >= new Date('2020-01-01').getTime());
332
332
  assert.ok(ts <= new Date('2020-12-31').getTime());
333
333
  });
334
+
335
+ test('countdown future returns direction and remaining', async () => {
336
+ const future = new Date(Date.now() + 10 * 24 * 3600 * 1000).toISOString();
337
+ const { status, body } = await getJson(`/v4/time?type=countdown&target=${encodeURIComponent(future)}`);
338
+ assert.equal(status, 200);
339
+ assert.equal(body.direction, 'future');
340
+ assert.ok(typeof body.human === 'string');
341
+ });
342
+
343
+ test('countdown missing target returns 400', async () => {
344
+ const { status } = await getJson('/v4/time?type=countdown');
345
+ assert.equal(status, 400);
346
+ });
334
347
  });
335
348
 
336
349
  describe('GET /v4/username', () => {
@@ -697,6 +710,49 @@ describe('GET /v4/password', () => {
697
710
  });
698
711
  });
699
712
 
713
+ describe('GET /v4/cron', () => {
714
+ test('returns next dates for * * * * *', async () => {
715
+ const { status, body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*');
716
+ assert.equal(status, 200);
717
+ assert.equal((body.next as string[]).length, 5);
718
+ assert.ok(typeof body.description === 'string');
719
+ });
720
+
721
+ test('count param is respected', async () => {
722
+ const { body } = await getJson('/v4/cron?expr=*%20*%20*%20*%20*&count=3');
723
+ assert.equal((body.next as string[]).length, 3);
724
+ });
725
+
726
+ test('invalid expression returns 400', async () => {
727
+ const { status } = await getJson('/v4/cron?expr=invalid');
728
+ assert.equal(status, 400);
729
+ });
730
+
731
+ test('missing expr returns 400', async () => {
732
+ const { status } = await getJson('/v4/cron');
733
+ assert.equal(status, 400);
734
+ });
735
+ });
736
+
737
+ describe('GET /v4/regex', () => {
738
+ test('basic match returns count and matches', async () => {
739
+ const { status, body } = await getJson('/v4/regex?pattern=hello&text=hello%20world');
740
+ assert.equal(status, 200);
741
+ assert.equal(body.valid, true);
742
+ assert.ok((body.count as number) >= 1);
743
+ });
744
+
745
+ test('invalid pattern returns valid: false with 200', async () => {
746
+ const { status, body } = await getJson('/v4/regex?pattern=%5Binvalid&text=hello');
747
+ assert.equal(status, 200);
748
+ assert.equal(body.valid, false);
749
+ });
750
+
751
+ test('missing pattern returns 400', async () => {
752
+ const { status } = await getJson('/v4/regex?text=hello');
753
+ assert.equal(status, 400);
754
+ });
755
+ });
700
756
 
701
757
  describe('Prototype access on dynamic endpoints', () => {
702
758
  test('algorithms?method=toString returns 400', async () => {
@@ -0,0 +1,110 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
4
+ import cron from '../../src/modules/v4/cron.js';
5
+
6
+ // Fixed reference point: a known Monday at 08:45 UTC
7
+ const FROM = '2026-01-05T08:45:00Z'; // Monday
8
+
9
+ describe('cron', () => {
10
+ describe('every minute', () => {
11
+ test('* * * * * returns 5 consecutive minutes', () => {
12
+ const r = cron('* * * * *', 5, FROM);
13
+ assert.equal(r.next.length, 5);
14
+ // First result should be the next minute after FROM
15
+ assert.ok(r.next[0]!.startsWith('2026-01-05T08:46'));
16
+ });
17
+
18
+ test('description is "Every minute"', () => {
19
+ const r = cron('* * * * *', 1, FROM);
20
+ assert.equal(r.description, 'Every minute');
21
+ });
22
+ });
23
+
24
+ describe('every N minutes', () => {
25
+ test('*/15 * * * * returns results 15 minutes apart', () => {
26
+ const r = cron('*/15 * * * *', 3, FROM);
27
+ assert.equal(r.next.length, 3);
28
+ assert.ok(r.next[0]!.includes('T09:00'));
29
+ assert.ok(r.next[1]!.includes('T09:15'));
30
+ assert.ok(r.next[2]!.includes('T09:30'));
31
+ });
32
+
33
+ test('description mentions every 15 minutes', () => {
34
+ const r = cron('*/15 * * * *', 1, FROM);
35
+ assert.match(r.description, /Every 15 minutes/);
36
+ });
37
+ });
38
+
39
+ describe('at fixed time', () => {
40
+ test('0 9 * * 1-5 fires at 09:00 on weekdays', () => {
41
+ const r = cron('0 9 * * 1-5', 3, FROM);
42
+ // Next Monday 09:00 is same day
43
+ assert.ok(r.next[0]!.includes('T09:00'));
44
+ // All results should be on weekdays (Mon–Fri)
45
+ for (const date of r.next) {
46
+ const d = new Date(date);
47
+ const dow = d.getUTCDay();
48
+ assert.ok(dow >= 1 && dow <= 5, `Expected weekday, got ${dow}`);
49
+ }
50
+ });
51
+
52
+ test('description includes Monday through Friday', () => {
53
+ const r = cron('0 9 * * 1-5', 1, FROM);
54
+ assert.match(r.description, /Monday through Friday/);
55
+ });
56
+ });
57
+
58
+ describe('plan example', () => {
59
+ test('*/15 9-17 * * 1-5 fires between 09:00 and 17:45 on weekdays', () => {
60
+ const r = cron('*/15 9-17 * * 1-5', 5, FROM);
61
+ assert.equal(r.next.length, 5);
62
+ for (const date of r.next) {
63
+ const d = new Date(date);
64
+ const hour = d.getUTCHours();
65
+ assert.ok(hour >= 9 && hour <= 17);
66
+ const dow = d.getUTCDay();
67
+ assert.ok(dow >= 1 && dow <= 5);
68
+ }
69
+ });
70
+
71
+ test('description includes "Every 15 minutes"', () => {
72
+ const r = cron('*/15 9-17 * * 1-5', 1, FROM);
73
+ assert.match(r.description, /Every 15 minutes/);
74
+ });
75
+ });
76
+
77
+ describe('count', () => {
78
+ test('count=10 returns 10 results', () => {
79
+ const r = cron('* * * * *', 10, FROM);
80
+ assert.equal(r.next.length, 10);
81
+ });
82
+
83
+ test('count=1 returns 1 result', () => {
84
+ const r = cron('*/30 * * * *', 1, FROM);
85
+ assert.equal(r.next.length, 1);
86
+ });
87
+ });
88
+
89
+ describe('errors', () => {
90
+ test('missing expr throws', () => {
91
+ assert.throws(() => cron(''), /expression/);
92
+ });
93
+
94
+ test('too few fields throws', () => {
95
+ assert.throws(() => cron('* * * *'), /5 fields/);
96
+ });
97
+
98
+ test('invalid range throws', () => {
99
+ assert.throws(() => cron('60-70 * * * *'), /valid: 0-59/);
100
+ });
101
+
102
+ test('invalid timezone throws', () => {
103
+ assert.throws(() => cron('* * * * *', 5, undefined, 'Mars/Olympus'), /timezone/);
104
+ });
105
+
106
+ test('count out of range throws', () => {
107
+ assert.throws(() => cron('* * * * *', 21), /Count/);
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,95 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
4
+ import regex from '../../src/modules/v4/regex.js';
5
+
6
+ describe('regex', () => {
7
+ describe('basic matching', () => {
8
+ test('finds a simple match', () => {
9
+ const r = regex('hello', 'hello world');
10
+ assert.equal(r.valid, true);
11
+ assert.equal(r.count, 1);
12
+ assert.equal(r.matches[0]!.match, 'hello');
13
+ assert.equal(r.matches[0]!.index, 0);
14
+ });
15
+
16
+ test('finds multiple matches', () => {
17
+ const r = regex('\\d+', 'abc 42 def 7');
18
+ assert.equal(r.count, 2);
19
+ assert.equal(r.matches[0]!.match, '42');
20
+ assert.equal(r.matches[1]!.match, '7');
21
+ });
22
+
23
+ test('no match returns empty array', () => {
24
+ const r = regex('xyz', 'hello world');
25
+ assert.equal(r.valid, true);
26
+ assert.equal(r.count, 0);
27
+ assert.deepEqual(r.matches, []);
28
+ });
29
+ });
30
+
31
+ describe('flags', () => {
32
+ test('flag i enables case-insensitive matching', () => {
33
+ const r = regex('hello', 'HELLO world', 'i');
34
+ assert.equal(r.count, 1);
35
+ assert.equal(r.matches[0]!.match, 'HELLO');
36
+ });
37
+
38
+ test('invalid flag x is silently ignored', () => {
39
+ const r = regex('hello', 'hello world', 'x');
40
+ assert.equal(r.valid, true);
41
+ assert.equal(r.count, 1);
42
+ });
43
+
44
+ test('g flag is always present', () => {
45
+ const r = regex('a', 'aaa', '');
46
+ assert.ok(r.flags.includes('g'));
47
+ });
48
+ });
49
+
50
+ describe('capture groups', () => {
51
+ test('numbered groups are returned', () => {
52
+ const r = regex('(\\d{4})-(\\d{2})', '2026-05');
53
+ assert.deepEqual(r.matches[0]!.groups, ['2026', '05']);
54
+ });
55
+
56
+ test('named groups are returned', () => {
57
+ const r = regex('(?<year>\\d{4})-(?<month>\\d{2})', '2026-05');
58
+ assert.equal(r.matches[0]!.namedGroups.year, '2026');
59
+ assert.equal(r.matches[0]!.namedGroups.month, '05');
60
+ });
61
+
62
+ test('no groups returns empty arrays', () => {
63
+ const r = regex('hello', 'hello');
64
+ assert.deepEqual(r.matches[0]!.groups, []);
65
+ assert.deepEqual(r.matches[0]!.namedGroups, {});
66
+ });
67
+ });
68
+
69
+ describe('invalid pattern', () => {
70
+ test('invalid regex returns valid: false', () => {
71
+ const r = regex('[invalid', 'hello');
72
+ assert.equal(r.valid, false);
73
+ assert.equal(r.count, 0);
74
+ assert.deepEqual(r.matches, []);
75
+ });
76
+ });
77
+
78
+ describe('errors', () => {
79
+ test('missing pattern throws', () => {
80
+ assert.throws(() => regex('', 'hello'), /pattern/);
81
+ });
82
+
83
+ test('missing text throws', () => {
84
+ assert.throws(() => regex('hello', ''), /text/);
85
+ });
86
+
87
+ test('pattern too long throws', () => {
88
+ assert.throws(() => regex('a'.repeat(201), 'hello'), /200/);
89
+ });
90
+
91
+ test('text too long throws', () => {
92
+ assert.throws(() => regex('hello', 'a'.repeat(1001)), /1000/);
93
+ });
94
+ });
95
+ });
@@ -65,4 +65,51 @@ describe('time', () => {
65
65
  const result = time('live', undefined, undefined, undefined, 'Europe/Paris');
66
66
  assert.equal(result.timezone, 'Europe/Paris');
67
67
  });
68
+
69
+ describe('countdown', () => {
70
+ test('future date returns direction future', () => {
71
+ const future = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
72
+ const r = time('countdown', undefined, undefined, undefined, undefined, future);
73
+ assert.equal(r.direction, 'future');
74
+ });
75
+
76
+ test('past date returns direction past', () => {
77
+ const past = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
78
+ const r = time('countdown', undefined, undefined, undefined, undefined, past);
79
+ assert.equal(r.direction, 'past');
80
+ });
81
+
82
+ test('remaining breakdown is correct', () => {
83
+ const target = new Date(Date.now() + 2 * 86400 * 1000 + 3 * 3600 * 1000).toISOString();
84
+ const r = time('countdown', undefined, undefined, undefined, undefined, target);
85
+ const rem = r.remaining as { days: number; hours: number };
86
+ assert.equal(rem.days, 2);
87
+ assert.equal(rem.hours, 3);
88
+ });
89
+
90
+ test('human skips zero units', () => {
91
+ const target = new Date(Date.now() + 3 * 86400 * 1000).toISOString();
92
+ const r = time('countdown', undefined, undefined, undefined, undefined, target);
93
+ assert.ok(!(r.human as string).includes('hour'));
94
+ assert.ok(!(r.human as string).includes('minute'));
95
+ });
96
+
97
+ test('throws on missing target', () => {
98
+ assert.throws(() => time('countdown'), /target date/);
99
+ });
100
+
101
+ test('throws on invalid target', () => {
102
+ assert.throws(
103
+ () => time('countdown', undefined, undefined, undefined, undefined, 'not-a-date'),
104
+ /valid target date/,
105
+ );
106
+ });
107
+
108
+ test('throws if target is more than 100 years away', () => {
109
+ assert.throws(
110
+ () => time('countdown', undefined, undefined, undefined, undefined, '2200-01-01'),
111
+ /100 years/,
112
+ );
113
+ });
114
+ });
68
115
  });