@20syldev/api 3.5.0 → 4.1.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 (213) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +32 -33
  3. package/dist/app.js +36 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/config/env.js +73 -0
  6. package/dist/config/env.js.map +1 -0
  7. package/dist/config/versions.js +72 -0
  8. package/dist/config/versions.js.map +1 -0
  9. package/dist/constants.js +42 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/middleware/cors.js +15 -0
  12. package/dist/middleware/cors.js.map +1 -0
  13. package/dist/middleware/error.js +6 -0
  14. package/dist/middleware/error.js.map +1 -0
  15. package/dist/middleware/json.js +8 -0
  16. package/dist/middleware/json.js.map +1 -0
  17. package/dist/middleware/logger.js +26 -0
  18. package/dist/middleware/logger.js.map +1 -0
  19. package/dist/middleware/ratelimit.js +58 -0
  20. package/dist/middleware/ratelimit.js.map +1 -0
  21. package/dist/middleware/version.js +30 -0
  22. package/dist/middleware/version.js.map +1 -0
  23. package/dist/modules/v3/algorithms.js +221 -0
  24. package/dist/modules/v3/algorithms.js.map +1 -0
  25. package/{modules → dist/modules}/v3/captcha.js +4 -13
  26. package/dist/modules/v3/captcha.js.map +1 -0
  27. package/dist/modules/v3/chat.js +103 -0
  28. package/dist/modules/v3/chat.js.map +1 -0
  29. package/{modules → dist/modules}/v3/color.js +12 -13
  30. package/dist/modules/v3/color.js.map +1 -0
  31. package/dist/modules/v3/convert.js +41 -0
  32. package/dist/modules/v3/convert.js.map +1 -0
  33. package/dist/modules/v3/domain.js +83 -0
  34. package/dist/modules/v3/domain.js.map +1 -0
  35. package/dist/modules/v3/hash.js +16 -0
  36. package/dist/modules/v3/hash.js.map +1 -0
  37. package/dist/modules/v3/hyperplanning.js +50 -0
  38. package/dist/modules/v3/hyperplanning.js.map +1 -0
  39. package/dist/modules/v3/levenshtein.js +38 -0
  40. package/dist/modules/v3/levenshtein.js.map +1 -0
  41. package/dist/modules/v3/personal.js +129 -0
  42. package/dist/modules/v3/personal.js.map +1 -0
  43. package/dist/modules/v3/qrcode.js +21 -0
  44. package/dist/modules/v3/qrcode.js.map +1 -0
  45. package/dist/modules/v3/tic_tac_toe.js +221 -0
  46. package/dist/modules/v3/tic_tac_toe.js.map +1 -0
  47. package/dist/modules/v3/time.js +88 -0
  48. package/dist/modules/v3/time.js.map +1 -0
  49. package/{modules → dist/modules}/v3/token.js +10 -11
  50. package/dist/modules/v3/token.js.map +1 -0
  51. package/dist/modules/v3/username.js +92 -0
  52. package/dist/modules/v3/username.js.map +1 -0
  53. package/dist/modules/v3/utils.js +70 -0
  54. package/dist/modules/v3/utils.js.map +1 -0
  55. package/dist/modules/v3.js +16 -0
  56. package/dist/modules/v3.js.map +1 -0
  57. package/dist/modules/v4/algorithms.js +200 -0
  58. package/dist/modules/v4/algorithms.js.map +1 -0
  59. package/dist/modules/v4/captcha.js +35 -0
  60. package/dist/modules/v4/captcha.js.map +1 -0
  61. package/dist/modules/v4/chat.js +92 -0
  62. package/dist/modules/v4/chat.js.map +1 -0
  63. package/dist/modules/v4/color.js +43 -0
  64. package/dist/modules/v4/color.js.map +1 -0
  65. package/dist/modules/v4/convert.js +33 -0
  66. package/dist/modules/v4/convert.js.map +1 -0
  67. package/dist/modules/v4/dice.js +30 -0
  68. package/dist/modules/v4/dice.js.map +1 -0
  69. package/dist/modules/v4/domain.js +78 -0
  70. package/dist/modules/v4/domain.js.map +1 -0
  71. package/dist/modules/v4/encode.js +165 -0
  72. package/dist/modules/v4/encode.js.map +1 -0
  73. package/dist/modules/v4/geo.js +41 -0
  74. package/dist/modules/v4/geo.js.map +1 -0
  75. package/dist/modules/v4/hash.js +9 -0
  76. package/dist/modules/v4/hash.js.map +1 -0
  77. package/dist/modules/v4/hyperplanning.js +42 -0
  78. package/dist/modules/v4/hyperplanning.js.map +1 -0
  79. package/dist/modules/v4/levenshtein.js +29 -0
  80. package/dist/modules/v4/levenshtein.js.map +1 -0
  81. package/dist/modules/v4/palette.js +89 -0
  82. package/dist/modules/v4/palette.js.map +1 -0
  83. package/dist/modules/v4/personal.js +125 -0
  84. package/dist/modules/v4/personal.js.map +1 -0
  85. package/dist/modules/v4/placeholder.js +96 -0
  86. package/dist/modules/v4/placeholder.js.map +1 -0
  87. package/dist/modules/v4/qrcode.js +12 -0
  88. package/dist/modules/v4/qrcode.js.map +1 -0
  89. package/dist/modules/v4/statistics.js +41 -0
  90. package/dist/modules/v4/statistics.js.map +1 -0
  91. package/dist/modules/v4/text.js +293 -0
  92. package/dist/modules/v4/text.js.map +1 -0
  93. package/dist/modules/v4/tic_tac_toe.js +206 -0
  94. package/dist/modules/v4/tic_tac_toe.js.map +1 -0
  95. package/dist/modules/v4/time.js +73 -0
  96. package/dist/modules/v4/time.js.map +1 -0
  97. package/dist/modules/v4/token.js +36 -0
  98. package/dist/modules/v4/token.js.map +1 -0
  99. package/dist/modules/v4/username.js +87 -0
  100. package/dist/modules/v4/username.js.map +1 -0
  101. package/dist/modules/v4/utils.js +26 -0
  102. package/dist/modules/v4/utils.js.map +1 -0
  103. package/dist/modules/v4/validate.js +54 -0
  104. package/dist/modules/v4/validate.js.map +1 -0
  105. package/dist/modules/v4.js +18 -0
  106. package/dist/modules/v4.js.map +1 -0
  107. package/dist/routes/delete.js +65 -0
  108. package/dist/routes/delete.js.map +1 -0
  109. package/dist/routes/get.js +441 -0
  110. package/dist/routes/get.js.map +1 -0
  111. package/dist/routes/index.js +50 -0
  112. package/dist/routes/index.js.map +1 -0
  113. package/dist/routes/patch.js +41 -0
  114. package/dist/routes/patch.js.map +1 -0
  115. package/dist/routes/post.js +185 -0
  116. package/dist/routes/post.js.map +1 -0
  117. package/dist/storage/index.js +13 -0
  118. package/dist/storage/index.js.map +1 -0
  119. package/dist/types/storage.js +2 -0
  120. package/dist/types/storage.js.map +1 -0
  121. package/dist/utils/response.js +10 -0
  122. package/dist/utils/response.js.map +1 -0
  123. package/dist/utils/version.js +3 -0
  124. package/dist/utils/version.js.map +1 -0
  125. package/docs/changelog.md +333 -0
  126. package/eslint.config.js +20 -0
  127. package/package.json +31 -15
  128. package/src/app.ts +48 -0
  129. package/src/config/env.ts +81 -0
  130. package/src/config/versions.ts +92 -0
  131. package/src/constants.ts +47 -0
  132. package/src/middleware/cors.ts +17 -0
  133. package/src/middleware/error.ts +7 -0
  134. package/src/middleware/json.ts +9 -0
  135. package/src/middleware/logger.ts +31 -0
  136. package/src/middleware/ratelimit.ts +66 -0
  137. package/src/middleware/version.ts +39 -0
  138. package/{modules → src/modules}/v3/algorithms.js +1 -1
  139. package/src/modules/v3/captcha.js +60 -0
  140. package/{modules → src/modules}/v3/chat.js +6 -4
  141. package/src/modules/v3/color.js +60 -0
  142. package/{modules → src/modules}/v3/convert.js +5 -5
  143. package/{modules → src/modules}/v3/domain.js +51 -4
  144. package/{modules → src/modules}/v3/hash.js +1 -1
  145. package/{modules → src/modules}/v3/hyperplanning.js +11 -10
  146. package/{modules → src/modules}/v3/levenshtein.js +3 -3
  147. package/{modules → src/modules}/v3/personal.js +20 -8
  148. package/{modules → src/modules}/v3/qrcode.js +1 -1
  149. package/{modules → src/modules}/v3/tic_tac_toe.js +24 -18
  150. package/{modules → src/modules}/v3/time.js +22 -11
  151. package/src/modules/v3/token.js +54 -0
  152. package/src/modules/v3/username.js +94 -0
  153. package/{modules → src/modules}/v3/utils.js +2 -2
  154. package/{modules → src/modules}/v3.js +1 -1
  155. package/src/modules/v4/algorithms.ts +197 -0
  156. package/src/modules/v4/captcha.ts +49 -0
  157. package/src/modules/v4/chat.ts +122 -0
  158. package/src/modules/v4/color.ts +64 -0
  159. package/src/modules/v4/convert.ts +38 -0
  160. package/src/modules/v4/dice.ts +39 -0
  161. package/src/modules/v4/domain.ts +81 -0
  162. package/src/modules/v4/encode.ts +170 -0
  163. package/src/modules/v4/geo.ts +53 -0
  164. package/src/modules/v4/hash.ts +9 -0
  165. package/src/modules/v4/hyperplanning.ts +58 -0
  166. package/src/modules/v4/levenshtein.ts +38 -0
  167. package/src/modules/v4/palette.ts +103 -0
  168. package/src/modules/v4/personal.ts +147 -0
  169. package/src/modules/v4/placeholder.ts +122 -0
  170. package/src/modules/v4/qrcode.ts +11 -0
  171. package/src/modules/v4/statistics.ts +55 -0
  172. package/src/modules/v4/text.ts +300 -0
  173. package/src/modules/v4/tic_tac_toe.ts +255 -0
  174. package/src/modules/v4/time.ts +92 -0
  175. package/src/modules/v4/token.ts +43 -0
  176. package/src/modules/v4/username.ts +89 -0
  177. package/src/modules/v4/utils.ts +38 -0
  178. package/src/modules/v4/validate.ts +55 -0
  179. package/src/modules/v4.ts +17 -0
  180. package/src/routes/delete.ts +72 -0
  181. package/src/routes/get.ts +508 -0
  182. package/src/routes/index.ts +58 -0
  183. package/src/routes/patch.ts +45 -0
  184. package/src/routes/post.ts +200 -0
  185. package/src/storage/index.ts +16 -0
  186. package/src/types/express.d.ts +17 -0
  187. package/src/types/storage.ts +48 -0
  188. package/src/utils/response.ts +11 -0
  189. package/src/utils/version.ts +5 -0
  190. package/tests/integration/api.test.ts +395 -0
  191. package/tests/tsconfig.json +3 -0
  192. package/tests/unit/algorithms.test.ts +120 -0
  193. package/tests/unit/color.test.ts +33 -0
  194. package/tests/unit/convert.test.ts +42 -0
  195. package/tests/unit/dice.test.ts +57 -0
  196. package/tests/unit/domain.test.ts +47 -0
  197. package/tests/unit/encode.test.ts +108 -0
  198. package/tests/unit/geo.test.ts +45 -0
  199. package/tests/unit/hash.test.ts +37 -0
  200. package/tests/unit/hyperplanning.test.ts +77 -0
  201. package/tests/unit/levenshtein.test.ts +34 -0
  202. package/tests/unit/palette.test.ts +53 -0
  203. package/tests/unit/personal.test.ts +65 -0
  204. package/tests/unit/statistics.test.ts +57 -0
  205. package/tests/unit/text.test.ts +107 -0
  206. package/tests/unit/time.test.ts +67 -0
  207. package/tests/unit/token.test.ts +61 -0
  208. package/tests/unit/username.test.ts +34 -0
  209. package/tests/unit/validate.test.ts +71 -0
  210. package/tsconfig.json +17 -0
  211. package/tsconfig.test.json +9 -0
  212. package/app.js +0 -865
  213. package/modules/v3/username.js +0 -32
@@ -0,0 +1,255 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { SESSION_TTL, GAME_CLEANUP_TTL, RATE_LIMIT_WINDOW, RATE_LIMIT_MAX } from '../../constants.js';
3
+ import type { TicTacToeStorage, TicTacToeMove, TicTacToeGame } from '../../types/storage.js';
4
+
5
+ interface TicTacToeParams {
6
+ username?: string;
7
+ move?: string;
8
+ session?: string;
9
+ game?: string;
10
+ private?: boolean;
11
+ storage: TicTacToeStorage;
12
+ }
13
+
14
+ interface GameResult {
15
+ winner?: string | null;
16
+ loser?: string | null;
17
+ tie?: boolean;
18
+ }
19
+
20
+ export default function tic_tac_toe(action: string, params: TicTacToeParams): Record<string, unknown> {
21
+ const storage = params.storage;
22
+
23
+ storage.games ??= {};
24
+ storage.sessions ??= {};
25
+ storage.rateLimits ??= {};
26
+
27
+ const { games, sessions, rateLimits } = storage;
28
+
29
+ if (action === 'list') return listGames(games);
30
+
31
+ if (!params.username) throw new Error('Please provide a username');
32
+
33
+ const u = params.username.toLowerCase();
34
+ const now = Date.now();
35
+
36
+ rateLimits[u] = (rateLimits[u] ?? []).filter((ts) => now - ts < RATE_LIMIT_WINDOW);
37
+ if (rateLimits[u]!.length > RATE_LIMIT_MAX) {
38
+ const remainingTime = Math.ceil((rateLimits[u]![0]! + RATE_LIMIT_WINDOW - now) / 1000);
39
+ throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
40
+ }
41
+ rateLimits[u]!.push(now);
42
+
43
+ if (action === 'play') {
44
+ return playMove(params, games, sessions, u, now);
45
+ } else if (action === 'fetch') {
46
+ return fetchGame(params, games, u);
47
+ } else if (action === 'forfeit') {
48
+ return forfeitGame(params, games, sessions);
49
+ } else {
50
+ throw new Error('Invalid action. Use "play", "fetch", "list", or "forfeit"');
51
+ }
52
+ }
53
+
54
+ function forfeitGame(
55
+ params: TicTacToeParams,
56
+ games: Record<string, TicTacToeGame>,
57
+ sessions: Record<string, { user: string; last: number }>,
58
+ ): Record<string, unknown> {
59
+ const { game, session } = params;
60
+
61
+ if (!game) throw new Error('Please provide a valid game ID');
62
+ if (!session) throw new Error('Please provide a valid session ID');
63
+ if (!games[game]) throw new Error('Game not found');
64
+
65
+ const u = params.username!.toLowerCase();
66
+ if (sessions[u] && sessions[u].user !== session) {
67
+ throw new Error('Session ID mismatch');
68
+ }
69
+
70
+ const players = games[game]!.players;
71
+ if (!players.includes(u)) throw new Error('You are not a player in this game');
72
+
73
+ const winner = players.find((p) => p !== u) ?? null;
74
+ delete games[game];
75
+ delete sessions[u];
76
+
77
+ return {
78
+ message: `${params.username} forfeited the game.${winner ? ` ${winner} wins.` : ''}`,
79
+ winner,
80
+ loser: params.username,
81
+ };
82
+ }
83
+
84
+ function playMove(
85
+ params: TicTacToeParams,
86
+ games: Record<string, TicTacToeGame>,
87
+ sessions: Record<string, { user: string; last: number }>,
88
+ u: string,
89
+ now: number,
90
+ ): Record<string, unknown> {
91
+ const { move, session, game } = params;
92
+ const validMoves = ['1-1', '1-2', '1-3', '2-1', '2-2', '2-3', '3-1', '3-2', '3-3'];
93
+
94
+ if (!move) throw new Error('Please provide a valid move');
95
+ if (!session) throw new Error('Please provide a valid session ID');
96
+ if (!game) throw new Error('Please provide a valid game ID');
97
+ if (!validMoves.includes(move)) throw new Error('Invalid move. Please provide a valid move (e.g., 1-1, 2-2, 3-3).');
98
+
99
+ if (sessions[u] && sessions[u].user !== session) {
100
+ throw new Error('Session ID mismatch');
101
+ }
102
+
103
+ games[game] = games[game] ?? {
104
+ moves: [],
105
+ players: [],
106
+ private: params.private || false,
107
+ creation: Date.now(),
108
+ };
109
+ const moves = games[game]!.moves;
110
+
111
+ const players = [...new Set(moves.map((play) => play.username))];
112
+ if (players.length >= 2 && !players.includes(params.username!)) {
113
+ throw new Error('Game is full, you can only watch.');
114
+ }
115
+
116
+ if (moves.length > 0 && moves[moves.length - 1]!.username === params.username) {
117
+ throw new Error('Please wait for the other player to make a move.');
118
+ }
119
+
120
+ if (moves.some((play) => play.move === move)) {
121
+ throw new Error('Move already made. Please choose a different move.');
122
+ }
123
+
124
+ const play: TicTacToeMove = { username: params.username!, move, session };
125
+ moves.push(play);
126
+
127
+ const result = checkGame(moves);
128
+ if (result.winner || result.tie) {
129
+ setTimeout(() => delete games[game!], GAME_CLEANUP_TTL);
130
+ return {
131
+ message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : "It's a tie."}`,
132
+ ...result,
133
+ };
134
+ }
135
+
136
+ setTimeout(() => delete games[game!], SESSION_TTL);
137
+
138
+ sessions[u] = sessions[u] ?? { user: session, last: now };
139
+ sessions[u]!.last = now;
140
+ setTimeout(() => {
141
+ if (sessions[u] && now - sessions[u].last >= SESSION_TTL) delete sessions[u];
142
+ }, SESSION_TTL);
143
+
144
+ return { message: 'Move sent successfully' };
145
+ }
146
+
147
+ function fetchGame(params: TicTacToeParams, games: Record<string, TicTacToeGame>, u: string): Record<string, unknown> {
148
+ const id = params.game || generateGameId();
149
+ if (!games[id]) {
150
+ games[id] = {
151
+ moves: [],
152
+ players: [],
153
+ private: params.private || false,
154
+ creation: Date.now(),
155
+ };
156
+ }
157
+ if (!games[id]!.players.includes(u)) {
158
+ games[id]!.players.push(u);
159
+ }
160
+
161
+ const moves = games[id]!.moves;
162
+ const players = games[id]!.players;
163
+ const privateGame = games[id]!.private;
164
+ const lastPlayer = moves.length ? moves[moves.length - 1]!.username : null;
165
+ const turn = players.find((p) => p !== lastPlayer) || players[0];
166
+ const status = players.length >= 2 ? 'ready' : 'waiting';
167
+ const result = moves.length ? checkGame(moves) : {};
168
+
169
+ return {
170
+ id,
171
+ moves,
172
+ players,
173
+ turn,
174
+ status,
175
+ private: privateGame,
176
+ ...result,
177
+ };
178
+ }
179
+
180
+ function listGames(games: Record<string, TicTacToeGame>): Record<string, unknown> {
181
+ const publicGames: Record<string, unknown>[] = [];
182
+ const now = Date.now();
183
+
184
+ for (const [gameId, game] of Object.entries(games)) {
185
+ if (!game.private) {
186
+ const result = game.moves.length ? checkGame(game.moves) : {};
187
+ const isFinished = result.winner || result.tie;
188
+
189
+ if (!isFinished) {
190
+ const lastPlayer = game.moves.length ? game.moves[game.moves.length - 1]!.username : null;
191
+ const turn = game.players.find((p) => p !== lastPlayer) || game.players[0];
192
+ const status = game.players.length >= 2 ? 'ready' : 'waiting';
193
+
194
+ publicGames.push({
195
+ id: gameId,
196
+ players: game.players,
197
+ playersCount: game.players.length,
198
+ moves: game.moves.length,
199
+ turn,
200
+ status,
201
+ creation: game.creation || now,
202
+ });
203
+ }
204
+ }
205
+ }
206
+
207
+ publicGames.sort((a, b) => (b.creation as number) - (a.creation as number));
208
+
209
+ return {
210
+ message: 'Public games available',
211
+ count: publicGames.length,
212
+ games: publicGames,
213
+ };
214
+ }
215
+
216
+ function generateGameId(): string {
217
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
218
+ return Array.from(randomBytes(5))
219
+ .map((b) => chars[b % chars.length])
220
+ .join('');
221
+ }
222
+
223
+ function checkGame(moves: TicTacToeMove[]): GameResult {
224
+ const board: (string | null)[][] = Array(3)
225
+ .fill(null)
226
+ .map(() => Array(3).fill(null) as (string | null)[]);
227
+ const playerSymbols: Record<string, string> = {};
228
+ const playersOrder: string[] = [];
229
+
230
+ moves.forEach(({ username, move }) => {
231
+ if (!playerSymbols[username]) {
232
+ playersOrder.push(username);
233
+ playerSymbols[username] = playersOrder.length === 1 ? 'X' : 'O';
234
+ }
235
+ const [row, col] = move.split('-').map(Number) as [number, number];
236
+ board[row - 1]![col - 1] = playerSymbols[username]!;
237
+ });
238
+
239
+ const checkWinner = (symbol: string): boolean => {
240
+ for (let i = 0; i < 3; i++) {
241
+ if (board[i]![0] === symbol && board[i]![1] === symbol && board[i]![2] === symbol) return true;
242
+ if (board[0]![i] === symbol && board[1]![i] === symbol && board[2]![i] === symbol) return true;
243
+ }
244
+ if (board[0]![0] === symbol && board[1]![1] === symbol && board[2]![2] === symbol) return true;
245
+ if (board[0]![2] === symbol && board[1]![1] === symbol && board[2]![0] === symbol) return true;
246
+ return false;
247
+ };
248
+
249
+ const winner = Object.keys(playerSymbols).find((player) => checkWinner(playerSymbols[player]!)) ?? null;
250
+ const isTie = !winner && moves.length === 9;
251
+ const loser =
252
+ winner && playersOrder.length === 2 ? (playersOrder.find((player) => player !== winner) ?? null) : null;
253
+
254
+ return { winner, loser, tie: isTie };
255
+ }
@@ -0,0 +1,92 @@
1
+ const validFormats = [
2
+ 'iso',
3
+ 'utc',
4
+ 'timestamp',
5
+ 'locale',
6
+ 'date',
7
+ 'time',
8
+ 'year',
9
+ 'month',
10
+ 'day',
11
+ 'hour',
12
+ 'minute',
13
+ 'second',
14
+ 'ms',
15
+ 'dayOfWeek',
16
+ 'dayOfYear',
17
+ 'weekNumber',
18
+ 'timezone',
19
+ 'timezoneOffset',
20
+ ] as const;
21
+
22
+ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'] as const;
23
+
24
+ type TimeFormat = (typeof validFormats)[number];
25
+ type Timezone = (typeof validTimezones)[number];
26
+
27
+ export default function time(
28
+ type: string = 'live',
29
+ start?: string,
30
+ end?: string,
31
+ format?: string,
32
+ timezone?: string,
33
+ ): Record<string, unknown> {
34
+ if (type !== 'live' && type !== 'random') {
35
+ throw new Error('Please provide a valid type (live or random)');
36
+ }
37
+
38
+ if (start && !Date.parse(start)) {
39
+ throw new Error('Please provide a valid start date (YYYY-MM-DD)');
40
+ }
41
+
42
+ if (end && !Date.parse(end)) {
43
+ throw new Error('Please provide a valid end date (YYYY-MM-DD)');
44
+ }
45
+
46
+ if (format && !validFormats.includes(format as TimeFormat)) {
47
+ throw new Error(`Please provide a valid format. Options: ${validFormats.join(', ')}`);
48
+ }
49
+
50
+ if (timezone && !validTimezones.includes(timezone as Timezone)) {
51
+ throw new Error(`Please provide a valid timezone. Options: ${validTimezones.join(', ')}`);
52
+ }
53
+
54
+ const getTimeFormats = (date: Date, tz: string): Record<string, unknown> => {
55
+ return {
56
+ iso: date.toISOString(),
57
+ utc: date.toUTCString(),
58
+ timestamp: date.getTime(),
59
+ locale: date.toLocaleString('en-US', { timeZone: tz, timeZoneName: 'long' }),
60
+ date: date.toLocaleDateString('en-US', { timeZone: tz }),
61
+ time: date.toLocaleTimeString('en-US', { timeZone: tz }),
62
+ year: date.getFullYear(),
63
+ month: date.getMonth() + 1,
64
+ day: date.getDate(),
65
+ hour: date.getHours(),
66
+ minute: date.getMinutes(),
67
+ second: date.getSeconds(),
68
+ ms: date.getMilliseconds(),
69
+ dayOfWeek: date.getDay(),
70
+ dayOfYear: Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000),
71
+ weekNumber: Math.ceil(((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 + 1) / 7),
72
+ timezone: tz,
73
+ timezoneOffset: date.getTimezoneOffset(),
74
+ };
75
+ };
76
+
77
+ if (type === 'random') {
78
+ const startDate = start ? new Date(start).getTime() : new Date('1900-01-01').getTime();
79
+ const endDate = end ? new Date(end).getTime() : new Date('2100-12-31').getTime();
80
+ const randomDate = new Date(startDate + Math.random() * (endDate - startDate));
81
+ const tz = timezone || validTimezones[Math.floor(Math.random() * validTimezones.length)]!;
82
+ const formats = getTimeFormats(randomDate, tz);
83
+
84
+ return format ? { date: formats[format] } : formats;
85
+ }
86
+
87
+ const now = new Date();
88
+ const tz = timezone || 'UTC';
89
+ const formats = getTimeFormats(now, tz);
90
+
91
+ return format ? { date: formats[format] } : formats;
92
+ }
@@ -0,0 +1,43 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { v4 } from 'uuid';
3
+ import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../../constants.js';
4
+
5
+ export default function token(len: number, type: string = 'alphanum'): string {
6
+ if (isNaN(len) || len < MIN_TOKEN_LENGTH) {
7
+ throw new Error('Length must be a number greater than or equal to 12');
8
+ }
9
+
10
+ if (len > MAX_TOKEN_LENGTH) {
11
+ throw new Error('Length cannot exceed 4096');
12
+ }
13
+
14
+ const genToken = (chars: string, length: number): string => {
15
+ return Array.from({ length }, () => {
16
+ return chars[Math.floor(Math.random() * chars.length)];
17
+ }).join('');
18
+ };
19
+
20
+ const tokenTypes: Record<string, () => string> = {
21
+ alpha: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
22
+ alphanum: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len),
23
+ base64: () =>
24
+ randomBytes(Math.ceil(len * 0.75))
25
+ .toString('base64')
26
+ .slice(0, len),
27
+ hex: () =>
28
+ randomBytes(Math.ceil(len * 0.5))
29
+ .toString('hex')
30
+ .slice(0, len),
31
+ num: () => genToken('0123456789', len),
32
+ punct: () => genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
33
+ urlsafe: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
34
+ uuid: () => v4().replace(/-/g, '').slice(0, len),
35
+ };
36
+
37
+ const lowerType = type.toLowerCase();
38
+ if (!tokenTypes[lowerType]) {
39
+ throw new Error(`Invalid token type. Valid types: ${Object.keys(tokenTypes).join(', ')}`);
40
+ }
41
+
42
+ return tokenTypes[lowerType]!();
43
+ }
@@ -0,0 +1,89 @@
1
+ import { random, randomNumber } from './utils.js';
2
+
3
+ export default function username(): Record<string, unknown> {
4
+ const adj = [
5
+ 'Happy',
6
+ 'Silly',
7
+ 'Clever',
8
+ 'Creative',
9
+ 'Brave',
10
+ 'Gentle',
11
+ 'Kind',
12
+ 'Funny',
13
+ 'Wise',
14
+ 'Charming',
15
+ 'Sincere',
16
+ 'Resourceful',
17
+ 'Patient',
18
+ 'Energetic',
19
+ 'Adventurous',
20
+ 'Ambitious',
21
+ 'Courageous',
22
+ 'Courteous',
23
+ 'Determined',
24
+ ];
25
+ const ani = [
26
+ 'Cat',
27
+ 'Dog',
28
+ 'Tiger',
29
+ 'Elephant',
30
+ 'Monkey',
31
+ 'Penguin',
32
+ 'Dolphin',
33
+ 'Lion',
34
+ 'Bear',
35
+ 'Fox',
36
+ 'Owl',
37
+ 'Giraffe',
38
+ 'Zebra',
39
+ 'Koala',
40
+ 'Rabbit',
41
+ 'Squirrel',
42
+ 'Panda',
43
+ 'Horse',
44
+ 'Wolf',
45
+ 'Eagle',
46
+ ];
47
+ const job = [
48
+ 'Writer',
49
+ 'Artist',
50
+ 'Musician',
51
+ 'Explorer',
52
+ 'Scientist',
53
+ 'Engineer',
54
+ 'Athlete',
55
+ 'Chef',
56
+ 'Doctor',
57
+ 'Teacher',
58
+ 'Lawyer',
59
+ 'Entrepreneur',
60
+ 'Actor',
61
+ 'Dancer',
62
+ 'Photographer',
63
+ 'Architect',
64
+ 'Pilot',
65
+ 'Designer',
66
+ 'Journalist',
67
+ 'Veterinarian',
68
+ ];
69
+
70
+ const nombre = randomNumber(0, 99);
71
+ const choix: Record<string, () => string> = {
72
+ adj_num: () => random(adj) + nombre,
73
+ ani_num: () => random(ani) + nombre,
74
+ pro_num: () => random(job) + nombre,
75
+ adj_ani: () => random(adj) + random(ani),
76
+ adj_ani_num: () => random(adj) + random(ani) + nombre,
77
+ adj_pro: () => random(adj) + random(job),
78
+ pro_ani: () => random(job) + random(ani),
79
+ pro_ani_num: () => random(job) + random(ani) + nombre,
80
+ };
81
+
82
+ return {
83
+ username: choix[random(Object.keys(choix))]!(),
84
+ number: nombre,
85
+ adjective: random(adj),
86
+ animal: random(ani),
87
+ job: random(job),
88
+ };
89
+ }
@@ -0,0 +1,38 @@
1
+ export function random<T>(arr: T[]): T {
2
+ return arr[Math.floor(Math.random() * arr.length)]!;
3
+ }
4
+
5
+ export function randomNumber(min: number, max: number): number {
6
+ return Math.floor(Math.random() * (max - min + 1)) + min;
7
+ }
8
+
9
+ export function genIP(): string {
10
+ return `${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`;
11
+ }
12
+
13
+ export function formatDate(date: Date): string {
14
+ return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
15
+ }
16
+
17
+ export function envList(key: string): string[] | null {
18
+ const v = process.env[key];
19
+ return v && v !== 'undefined' ? v.split(' ') : null;
20
+ }
21
+
22
+ export function checkRateLimit(
23
+ rateLimits: Record<string, number[]>,
24
+ userId: string,
25
+ timestamp: number,
26
+ window = 10000,
27
+ limit = 50,
28
+ ): boolean {
29
+ rateLimits[userId] = (rateLimits[userId] ?? []).filter((ts) => timestamp - ts < window);
30
+
31
+ if (rateLimits[userId]!.length > limit) {
32
+ const remainingTime = Math.ceil((rateLimits[userId]![0]! + window - timestamp) / 1000);
33
+ throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
34
+ }
35
+
36
+ rateLimits[userId]!.push(timestamp);
37
+ return false;
38
+ }
@@ -0,0 +1,55 @@
1
+ import { MAX_STRING_LENGTH } from '../../constants.js';
2
+
3
+ function checkValue(value: string): void {
4
+ if (!value) throw new Error('A value is required');
5
+ if (typeof value !== 'string') throw new Error('Value must be a string');
6
+ if (value.length > MAX_STRING_LENGTH)
7
+ throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
8
+ }
9
+
10
+ export function luhn(value: string): { valid: boolean; value: string } {
11
+ checkValue(value);
12
+ const digits = value.replace(/\s|-/g, '');
13
+ if (!/^\d+$/.test(digits)) throw new Error('Value must contain only digits, spaces or dashes');
14
+ if (digits.length < 12 || digits.length > 19) throw new Error('Card number must have between 12 and 19 digits');
15
+
16
+ let sum = 0;
17
+ let alt = false;
18
+ for (let i = digits.length - 1; i >= 0; i--) {
19
+ let n = parseInt(digits[i]!, 10);
20
+ if (alt) {
21
+ n *= 2;
22
+ if (n > 9) n -= 9;
23
+ }
24
+ sum += n;
25
+ alt = !alt;
26
+ }
27
+
28
+ return { valid: sum % 10 === 0, value: digits };
29
+ }
30
+
31
+ export function iban(value: string): { valid: boolean; value: string; country?: string } {
32
+ checkValue(value);
33
+ const cleaned = value.replace(/\s/g, '').toUpperCase();
34
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(cleaned)) throw new Error('Invalid IBAN format');
35
+ if (cleaned.length < 15 || cleaned.length > 34) throw new Error('IBAN length must be between 15 and 34 characters');
36
+
37
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
38
+ const numeric = rearranged
39
+ .split('')
40
+ .map((c) => (c >= 'A' && c <= 'Z' ? (c.charCodeAt(0) - 55).toString() : c))
41
+ .join('');
42
+
43
+ let remainder = 0;
44
+ for (const char of numeric) {
45
+ remainder = (remainder * 10 + parseInt(char, 10)) % 97;
46
+ }
47
+
48
+ return { valid: remainder === 1, value: cleaned, country: cleaned.slice(0, 2) };
49
+ }
50
+
51
+ export function email(value: string): { valid: boolean; value: string } {
52
+ checkValue(value);
53
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
54
+ return { valid: regex.test(value), value };
55
+ }
@@ -0,0 +1,17 @@
1
+ export * as algorithms from './v4/algorithms.js';
2
+ export { default as chat } from './v4/chat.js';
3
+ export { default as captcha } from './v4/captcha.js';
4
+ export { default as color } from './v4/color.js';
5
+ export { default as convert } from './v4/convert.js';
6
+ export { default as dice } from './v4/dice.js';
7
+ export { default as domain } from './v4/domain.js';
8
+ export { default as hash } from './v4/hash.js';
9
+ export { default as hyperplanning } from './v4/hyperplanning.js';
10
+ export { default as levenshtein } from './v4/levenshtein.js';
11
+ export { default as personal } from './v4/personal.js';
12
+ export { default as qrcode } from './v4/qrcode.js';
13
+ export { default as statistics } from './v4/statistics.js';
14
+ export { default as tic_tac_toe } from './v4/tic_tac_toe.js';
15
+ export { default as time } from './v4/time.js';
16
+ export { default as token } from './v4/token.js';
17
+ export { default as username } from './v4/username.js';
@@ -0,0 +1,72 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+ import { chatStorage, ticTacToeStorage } from '../storage/index.js';
3
+ import { error } from '../utils/response.js';
4
+ import { supportsRest } from '../utils/version.js';
5
+
6
+ const router = Router();
7
+
8
+ // Forfeit a tic-tac-toe game (REST: DELETE /tic-tac-toe/:game)
9
+ router.delete('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
10
+ if (!supportsRest(req)) {
11
+ error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/tic-tac-toe`);
12
+ return;
13
+ }
14
+
15
+ const game = req.params.game as string;
16
+ const { username, session } = (req.body as Record<string, string>) || {};
17
+
18
+ if (!username) {
19
+ error(res, 400, 'Please provide a username (?username={username})');
20
+ return;
21
+ }
22
+ if (!session) {
23
+ error(res, 400, 'Please provide a valid session ID (&session={ID})');
24
+ return;
25
+ }
26
+
27
+ try {
28
+ const result = req.module.tic_tac_toe('forfeit', {
29
+ username,
30
+ session,
31
+ game,
32
+ storage: ticTacToeStorage,
33
+ });
34
+ res.jsonResponse(result);
35
+ } catch (err) {
36
+ error(res, 400, (err as Error).message);
37
+ }
38
+ });
39
+
40
+ // Clear a private chat (REST: DELETE /chat/:token)
41
+ router.delete('/:version/chat/:token', (req: Request, res: Response) => {
42
+ if (!supportsRest(req)) {
43
+ error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/chat`);
44
+ return;
45
+ }
46
+
47
+ const token = req.params.token as string;
48
+ const { username, session } = (req.body as Record<string, string>) || {};
49
+
50
+ if (!username) {
51
+ error(res, 400, 'Please provide a username (?username={username})');
52
+ return;
53
+ }
54
+ if (!session) {
55
+ error(res, 400, 'Please provide a valid session ID (&session={ID})');
56
+ return;
57
+ }
58
+
59
+ try {
60
+ const result = req.module.chat('clear', {
61
+ username,
62
+ token,
63
+ session,
64
+ storage: chatStorage,
65
+ });
66
+ res.jsonResponse(result);
67
+ } catch (err) {
68
+ error(res, 400, (err as Error).message);
69
+ }
70
+ });
71
+
72
+ export default router;