@20syldev/api 4.8.0 → 5.0.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 (126) hide show
  1. package/README.md +93 -55
  2. package/dist/config/versions.js +16 -0
  3. package/dist/config/versions.js.map +1 -1
  4. package/dist/constants.js +5 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/modules/v5/chart.js +379 -0
  7. package/dist/modules/v5/chart.js.map +1 -0
  8. package/dist/modules/v5/evaluate.js +281 -0
  9. package/dist/modules/v5/evaluate.js.map +1 -0
  10. package/dist/modules/v5/matrix.js +238 -0
  11. package/dist/modules/v5/matrix.js.map +1 -0
  12. package/dist/modules/v5.js +5 -0
  13. package/dist/modules/v5.js.map +1 -0
  14. package/dist/routes/get.js +191 -162
  15. package/dist/routes/get.js.map +1 -1
  16. package/dist/routes/post.js +69 -0
  17. package/dist/routes/post.js.map +1 -1
  18. package/package.json +4 -2
  19. package/recap.md +98 -0
  20. package/.github/FUNDING.yml +0 -1
  21. package/.prettierrc +0 -7
  22. package/eslint.config.js +0 -24
  23. package/robots.txt +0 -74
  24. package/src/app.ts +0 -53
  25. package/src/config/env.ts +0 -87
  26. package/src/config/plans.ts +0 -61
  27. package/src/config/versions.ts +0 -121
  28. package/src/constants.ts +0 -61
  29. package/src/favicon.ico +0 -0
  30. package/src/middleware/cors.ts +0 -23
  31. package/src/middleware/error.ts +0 -16
  32. package/src/middleware/json.ts +0 -9
  33. package/src/middleware/logger.ts +0 -32
  34. package/src/middleware/ratelimit.ts +0 -89
  35. package/src/middleware/version.ts +0 -40
  36. package/src/modules/v3/algorithms.js +0 -218
  37. package/src/modules/v3/captcha.js +0 -60
  38. package/src/modules/v3/chat.js +0 -111
  39. package/src/modules/v3/color.js +0 -60
  40. package/src/modules/v3/convert.js +0 -39
  41. package/src/modules/v3/domain.js +0 -86
  42. package/src/modules/v3/hash.js +0 -16
  43. package/src/modules/v3/hyperplanning.js +0 -56
  44. package/src/modules/v3/levenshtein.js +0 -46
  45. package/src/modules/v3/personal.js +0 -139
  46. package/src/modules/v3/qrcode.js +0 -20
  47. package/src/modules/v3/tic_tac_toe.js +0 -242
  48. package/src/modules/v3/time.js +0 -98
  49. package/src/modules/v3/token.js +0 -54
  50. package/src/modules/v3/username.js +0 -94
  51. package/src/modules/v3/utils.js +0 -77
  52. package/src/modules/v3.js +0 -15
  53. package/src/modules/v4/address.ts +0 -412
  54. package/src/modules/v4/agent.ts +0 -104
  55. package/src/modules/v4/algorithms.ts +0 -276
  56. package/src/modules/v4/captcha.ts +0 -114
  57. package/src/modules/v4/chat.ts +0 -130
  58. package/src/modules/v4/color.ts +0 -75
  59. package/src/modules/v4/convert.ts +0 -127
  60. package/src/modules/v4/cron.ts +0 -221
  61. package/src/modules/v4/dice.ts +0 -46
  62. package/src/modules/v4/domain.ts +0 -86
  63. package/src/modules/v4/encode.ts +0 -241
  64. package/src/modules/v4/geo.ts +0 -64
  65. package/src/modules/v4/hash.ts +0 -32
  66. package/src/modules/v4/hyperplanning.ts +0 -93
  67. package/src/modules/v4/ip.ts +0 -134
  68. package/src/modules/v4/levenshtein.ts +0 -46
  69. package/src/modules/v4/palette.ts +0 -71
  70. package/src/modules/v4/password.ts +0 -379
  71. package/src/modules/v4/personal.ts +0 -140
  72. package/src/modules/v4/placeholder.ts +0 -197
  73. package/src/modules/v4/qrcode.ts +0 -121
  74. package/src/modules/v4/regex.ts +0 -67
  75. package/src/modules/v4/statistics.ts +0 -62
  76. package/src/modules/v4/text.ts +0 -330
  77. package/src/modules/v4/tic_tac_toe.ts +0 -264
  78. package/src/modules/v4/time.ts +0 -149
  79. package/src/modules/v4/token.ts +0 -53
  80. package/src/modules/v4/username.ts +0 -94
  81. package/src/modules/v4/validate.ts +0 -76
  82. package/src/modules/v4.ts +0 -29
  83. package/src/routes/delete.ts +0 -72
  84. package/src/routes/get.ts +0 -856
  85. package/src/routes/index.ts +0 -59
  86. package/src/routes/patch.ts +0 -45
  87. package/src/routes/post.ts +0 -209
  88. package/src/storage/index.ts +0 -16
  89. package/src/types/express.d.ts +0 -17
  90. package/src/types/storage.ts +0 -48
  91. package/src/utils/colors.ts +0 -91
  92. package/src/utils/helpers.ts +0 -90
  93. package/src/utils/response.ts +0 -28
  94. package/tests/integration/api.test.ts +0 -772
  95. package/tests/tsconfig.json +0 -3
  96. package/tests/unit/address.test.ts +0 -84
  97. package/tests/unit/agent.test.ts +0 -113
  98. package/tests/unit/algorithms.test.ts +0 -121
  99. package/tests/unit/captcha.test.ts +0 -62
  100. package/tests/unit/chat.test.ts +0 -54
  101. package/tests/unit/color.test.ts +0 -54
  102. package/tests/unit/convert.test.ts +0 -68
  103. package/tests/unit/cron.test.ts +0 -110
  104. package/tests/unit/dice.test.ts +0 -58
  105. package/tests/unit/domain.test.ts +0 -48
  106. package/tests/unit/encode.test.ts +0 -109
  107. package/tests/unit/geo.test.ts +0 -46
  108. package/tests/unit/hash.test.ts +0 -45
  109. package/tests/unit/hyperplanning.test.ts +0 -97
  110. package/tests/unit/ip.test.ts +0 -140
  111. package/tests/unit/levenshtein.test.ts +0 -35
  112. package/tests/unit/palette.test.ts +0 -54
  113. package/tests/unit/password.test.ts +0 -86
  114. package/tests/unit/personal.test.ts +0 -66
  115. package/tests/unit/placeholder.test.ts +0 -58
  116. package/tests/unit/qrcode.test.ts +0 -83
  117. package/tests/unit/regex.test.ts +0 -95
  118. package/tests/unit/statistics.test.ts +0 -58
  119. package/tests/unit/text.test.ts +0 -108
  120. package/tests/unit/tic_tac_toe.test.ts +0 -56
  121. package/tests/unit/time.test.ts +0 -115
  122. package/tests/unit/token.test.ts +0 -62
  123. package/tests/unit/username.test.ts +0 -35
  124. package/tests/unit/validate.test.ts +0 -72
  125. package/tsconfig.json +0 -17
  126. package/tsconfig.test.json +0 -9
@@ -1,59 +0,0 @@
1
- import { type Request, type Response, Router } from 'express';
2
-
3
- import { versions } from '../config/versions.js';
4
- import { APP_VERSION, DOCS_URL, START_TIME } from '../constants.js';
5
- import { logger } from '../middleware/logger.js';
6
- import { ipLimits } from '../storage/index.js';
7
-
8
- const router = Router();
9
-
10
- router.get('/', (req: Request, res: Response) => {
11
- const base = `${req.protocol}://${req.get('host')}`;
12
- const links = Object.keys(versions).reduce<Record<string, string>>((link, version) => {
13
- link[version] = `${base}/${version}`;
14
- return link;
15
- }, {});
16
-
17
- res.jsonResponse({
18
- documentation: DOCS_URL,
19
- latest: `${base}/latest`,
20
- health: `${base}/health`,
21
- logs: `${base}/logs`,
22
- versions: links,
23
- });
24
- });
25
-
26
- router.get('/health', (_req: Request, res: Response) => {
27
- const mem = process.memoryUsage();
28
-
29
- res.jsonResponse({
30
- status: 'ok',
31
- uptime: Math.floor((Date.now() - START_TIME) / 1000),
32
- version: APP_VERSION,
33
- node: process.version,
34
- memory: {
35
- rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MB`,
36
- heap_used: `${(mem.heapUsed / 1024 / 1024).toFixed(1)} MB`,
37
- heap_total: `${(mem.heapTotal / 1024 / 1024).toFixed(1)} MB`,
38
- },
39
- connections: Object.keys(ipLimits).length,
40
- logs: logger.entries().length,
41
- });
42
- });
43
-
44
- router.get('/logs', (_req: Request, res: Response) => {
45
- res.jsonResponse(logger.entries());
46
- });
47
-
48
- router.get('/latest', (_req: Request, res: Response) => {
49
- const latest = Object.keys(versions).pop()!;
50
- res.redirect(`/${latest}`);
51
- });
52
-
53
- router.get('/latest/{*rest}', (req: Request, res: Response) => {
54
- const latest = Object.keys(versions).pop()!;
55
- const rest = (req.params as Record<string, string>).rest;
56
- res.redirect(`/${latest}/${rest}`);
57
- });
58
-
59
- export default router;
@@ -1,45 +0,0 @@
1
- import { type Request, type Response, Router } from 'express';
2
-
3
- import { ticTacToeStorage } from '../storage/index.js';
4
- import { error } from '../utils/response.js';
5
-
6
- const router = Router();
7
-
8
- // Play a tic-tac-toe move
9
- router.patch('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
10
- if (req.version !== 'v4') {
11
- error(res, 405, 'PATCH 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, move, 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 (!move) {
23
- error(res, 400, 'Please provide a valid move (&move={move})');
24
- return;
25
- }
26
- if (!session) {
27
- error(res, 400, 'Please provide a valid session ID (&session={ID})');
28
- return;
29
- }
30
-
31
- try {
32
- const result = req.module.tic_tac_toe('play', {
33
- username,
34
- move,
35
- session,
36
- game,
37
- storage: ticTacToeStorage,
38
- });
39
- res.jsonResponse(result);
40
- } catch (err) {
41
- error(res, 400, (err as Error).message);
42
- }
43
- });
44
-
45
- export default router;
@@ -1,209 +0,0 @@
1
- import { type Request, type Response, Router } from 'express';
2
-
3
- import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../constants.js';
4
- import type { HashResult } from '../modules/v4/hash.js';
5
- import { chatStorage, ticTacToeStorage } from '../storage/index.js';
6
- import { since } from '../utils/helpers.js';
7
- import { error } from '../utils/response.js';
8
-
9
- const router = Router();
10
-
11
- // Store chat messages
12
- router.post('/:version/chat', (req: Request, res: Response) => {
13
- const { username, message, timestamp, session, token } = (req.body as Record<string, string>) || {};
14
-
15
- if (!username) {
16
- error(res, 400, 'Please provide a username (?username={username})');
17
- return;
18
- }
19
- if (!message) {
20
- error(res, 400, 'Please provide a message (&message={message})');
21
- return;
22
- }
23
- if (!session) {
24
- error(res, 400, 'Please provide a valid session ID (&session={ID})');
25
- return;
26
- }
27
-
28
- try {
29
- const result = req.module.chat('message', {
30
- username,
31
- message,
32
- timestamp,
33
- session,
34
- token,
35
- storage: chatStorage,
36
- });
37
- res.jsonResponse(result);
38
- } catch (err) {
39
- error(res, 400, (err as Error).message);
40
- }
41
- });
42
-
43
- // Display a private chat with a token
44
- router.post('/:version/chat/private', (req: Request, res: Response) => {
45
- const { username, token } = (req.body as Record<string, string>) || {};
46
-
47
- if (!username) {
48
- error(res, 400, 'Please provide a username (?username={username})');
49
- return;
50
- }
51
- if (!token) {
52
- error(res, 400, 'Please provide a valid token (&token={key}).');
53
- return;
54
- }
55
-
56
- try {
57
- const messages = req.module.chat('private', {
58
- username,
59
- token,
60
- storage: chatStorage,
61
- });
62
- res.jsonResponse(messages);
63
- } catch (err) {
64
- error(res, 400, (err as Error).message);
65
- }
66
- });
67
-
68
- // Generate hash
69
- router.post('/:version/hash', (req: Request, res: Response) => {
70
- const { text, method, encoding } = (req.body as Record<string, string>) || {};
71
-
72
- if (!text) {
73
- error(res, 400, 'Please provide a text (?text={text})', `${req.version}/hash`);
74
- return;
75
- }
76
- if (!method) {
77
- error(res, 400, 'Please provide a valid hash algorithm (&method={algorithm})', `${req.version}/hash`);
78
- return;
79
- }
80
-
81
- try {
82
- if (since(req.version, 4)) {
83
- const hashFn = req.module.hash as (t: string, m: string, e?: string) => HashResult;
84
- const result = hashFn(text, method, encoding);
85
- res.jsonResponse(result);
86
- } else {
87
- const result = (req.module.hash as (t: string, m: string) => Record<string, string>)(text, method);
88
- res.jsonResponse(result);
89
- }
90
- } catch (err) {
91
- error(res, 400, (err as Error).message, `${req.version}/hash`);
92
- }
93
- });
94
-
95
- // Display a planning from an ICS file
96
- router.post('/:version/hyperplanning', async (req: Request, res: Response) => {
97
- const { url, detail } = (req.body as Record<string, string>) || {};
98
-
99
- if (!url) {
100
- error(res, 400, 'Please provide a valid ICS file URL (?url={URL})', `${req.version}/hyperplanning`);
101
- return;
102
- }
103
-
104
- try {
105
- const hyperplanning = await req.module.hyperplanning(url, detail);
106
- res.jsonResponse(hyperplanning);
107
- } catch (err) {
108
- error(res, 400, (err as Error).message, `${req.version}/hyperplanning`);
109
- }
110
- });
111
-
112
- // Store tic tac toe games
113
- router.post('/:version/tic-tac-toe', (req: Request, res: Response) => {
114
- const { username, move, session, game } = (req.body as Record<string, string>) || {};
115
-
116
- if (!username) {
117
- error(res, 400, 'Please provide a username (?username={username})');
118
- return;
119
- }
120
- if (!move) {
121
- error(res, 400, 'Please provide a valid move (&move={move})');
122
- return;
123
- }
124
- if (!session) {
125
- error(res, 400, 'Please provide a valid session ID (&session={ID})');
126
- return;
127
- }
128
- if (!game) {
129
- error(res, 400, 'Please provide a valid game ID (&game={ID})');
130
- return;
131
- }
132
-
133
- try {
134
- const result = req.module.tic_tac_toe('play', {
135
- username,
136
- move,
137
- session,
138
- game,
139
- storage: ticTacToeStorage,
140
- });
141
- res.jsonResponse(result);
142
- } catch (err) {
143
- error(res, 400, (err as Error).message);
144
- }
145
- });
146
-
147
- // Display a tic tac toe game with a token
148
- router.post('/:version/tic-tac-toe/fetch', (req: Request, res: Response) => {
149
- const { username, game } = (req.body as Record<string, string>) || {};
150
- const privateGame = ((req.body as Record<string, unknown>) || {}).private as boolean | undefined;
151
-
152
- if (!username) {
153
- error(res, 400, 'Please provide a username (?username={username})');
154
- return;
155
- }
156
-
157
- try {
158
- const result = req.module.tic_tac_toe('fetch', {
159
- username,
160
- game,
161
- private: privateGame,
162
- storage: ticTacToeStorage,
163
- });
164
- res.jsonResponse(result);
165
- } catch (err) {
166
- error(res, 400, (err as Error).message);
167
- }
168
- });
169
-
170
- // List public tic tac toe games
171
- router.post('/:version/tic-tac-toe/list', (req: Request, res: Response) => {
172
- try {
173
- const result = req.module.tic_tac_toe('list', {
174
- storage: ticTacToeStorage,
175
- });
176
- res.jsonResponse(result);
177
- } catch (err) {
178
- error(res, 400, (err as Error).message);
179
- }
180
- });
181
-
182
- // Generate Token
183
- router.post('/:version/token', (req: Request, res: Response) => {
184
- const body = (req.body as Record<string, unknown>) || {};
185
- const len = parseInt(String(body.len || 24), 10);
186
- const type = body.type ? String(body.type).toLowerCase() : 'alpha';
187
-
188
- if (isNaN(len) || len < 0) {
189
- error(res, 400, 'Invalid number.', `${req.version}/token`);
190
- return;
191
- }
192
- if (len > MAX_TOKEN_LENGTH) {
193
- error(res, 400, 'Length cannot exceed 4096.', `${req.version}/token`);
194
- return;
195
- }
196
- if (len < MIN_TOKEN_LENGTH) {
197
- error(res, 400, 'Length cannot be less than 12.', `${req.version}/token`);
198
- return;
199
- }
200
-
201
- try {
202
- const token = req.module.token(len, type);
203
- res.jsonResponse({ token });
204
- } catch (err) {
205
- error(res, 400, (err as Error).message, `${req.version}/token`);
206
- }
207
- });
208
-
209
- export default router;
@@ -1,16 +0,0 @@
1
- import type { ChatStorage, IpLimits, TicTacToeStorage } from '../types/storage.js';
2
-
3
- export const ipLimits: IpLimits = {};
4
-
5
- export const chatStorage: ChatStorage = {
6
- messages: [],
7
- privateChats: {},
8
- sessions: {},
9
- rateLimits: {},
10
- };
11
-
12
- export const ticTacToeStorage: TicTacToeStorage = {
13
- games: {},
14
- sessions: {},
15
- rateLimits: {},
16
- };
@@ -1,17 +0,0 @@
1
- import type * as apiv3 from '../modules/v3.js';
2
- import type * as apiv4 from '../modules/v4.js';
3
-
4
- declare global {
5
- namespace Express {
6
- interface Request {
7
- version: string;
8
- latest: string;
9
- endpoint: string;
10
- module: typeof apiv3 | typeof apiv4;
11
- }
12
-
13
- interface Response {
14
- jsonResponse: (data: unknown) => void;
15
- }
16
- }
17
- }
@@ -1,48 +0,0 @@
1
- export interface LogEntry {
2
- timestamp: string;
3
- method: string;
4
- url: string;
5
- status: number;
6
- duration: string;
7
- platform: string | undefined;
8
- }
9
-
10
- export interface ChatMessage {
11
- username: string;
12
- message: string;
13
- timestamp: string;
14
- }
15
-
16
- export interface ChatStorage {
17
- messages: ChatMessage[];
18
- privateChats: Record<string, ChatMessage[]>;
19
- sessions: Record<string, { user: string; last: number }>;
20
- rateLimits: Record<string, number[]>;
21
- }
22
-
23
- export interface TicTacToeMove {
24
- username: string;
25
- move: string;
26
- session: string;
27
- }
28
-
29
- export interface TicTacToeGame {
30
- moves: TicTacToeMove[];
31
- players: string[];
32
- private: boolean;
33
- creation: number;
34
- }
35
-
36
- export interface TicTacToeStorage {
37
- games: Record<string, TicTacToeGame>;
38
- sessions: Record<string, { user: string; last: number }>;
39
- rateLimits: Record<string, number[]>;
40
- }
41
-
42
- export interface IpLimits {
43
- [ip: string]: {
44
- [hour: string]: {
45
- [minute: string]: number;
46
- };
47
- };
48
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Parses a hex color string into an RGB tuple.
3
- *
4
- * @param hex - Hex color string (#RRGGBB or RRGGBB)
5
- * @returns RGB tuple [r, g, b] with values 0-255
6
- * @throws Error if the hex string is invalid
7
- */
8
- export function hexToRgb(hex: string): [number, number, number] {
9
- const clean = hex.replace('#', '');
10
- if (!/^[0-9a-fA-F]{6}$/.test(clean)) throw new Error('Invalid HEX color (use #RRGGBB)');
11
- return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];
12
- }
13
-
14
- /**
15
- * Converts HSL values to an RGB tuple.
16
- *
17
- * @param h - Hue in degrees (0-360)
18
- * @param s - Saturation (0-1)
19
- * @param l - Lightness (0-1)
20
- * @returns RGB tuple [r, g, b] with values 0-255
21
- */
22
- export function hslToRgb(h: number, s: number, l: number): [number, number, number] {
23
- const c = (1 - Math.abs(2 * l - 1)) * s;
24
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
25
- const m = l - c / 2;
26
- let r1 = 0,
27
- g1 = 0,
28
- b1 = 0;
29
- if (h < 60) [r1, g1, b1] = [c, x, 0];
30
- else if (h < 120) [r1, g1, b1] = [x, c, 0];
31
- else if (h < 180) [r1, g1, b1] = [0, c, x];
32
- else if (h < 240) [r1, g1, b1] = [0, x, c];
33
- else if (h < 300) [r1, g1, b1] = [x, 0, c];
34
- else [r1, g1, b1] = [c, 0, x];
35
- return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
36
- }
37
-
38
- /**
39
- * Validates and normalizes a hex color string, adding # if missing.
40
- *
41
- * @param value - Raw color value (with or without #)
42
- * @param def - Default color if value is empty
43
- * @returns Normalized hex color string with #
44
- * @throws Error if the color format is invalid
45
- */
46
- export function normalizeColor(value: string | undefined, def: string): string {
47
- if (!value) return def;
48
- const clean = value.startsWith('#') ? value : `#${value}`;
49
- if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(clean)) throw new Error('Invalid color (use hex like ff6600)');
50
- return clean;
51
- }
52
-
53
- /**
54
- * Formats an RGB tuple as a hex color string.
55
- *
56
- * @param r - Red channel (0-255)
57
- * @param g - Green channel (0-255)
58
- * @param b - Blue channel (0-255)
59
- * @returns Hex color string (#rrggbb)
60
- */
61
- export function rgbToHex(r: number, g: number, b: number): string {
62
- return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
63
- }
64
-
65
- /**
66
- * Converts RGB values to an HSL tuple.
67
- *
68
- * @param r - Red channel (0-255)
69
- * @param g - Green channel (0-255)
70
- * @param b - Blue channel (0-255)
71
- * @returns HSL tuple [h (0-360), s (0-1), l (0-1)]
72
- */
73
- export function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
74
- const r1 = r / 255,
75
- g1 = g / 255,
76
- b1 = b / 255;
77
- const max = Math.max(r1, g1, b1),
78
- min = Math.min(r1, g1, b1);
79
- const l = (max + min) / 2;
80
-
81
- if (max === min) return [0, 0, l];
82
-
83
- const d = max - min;
84
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
85
- let h = 0;
86
- if (max === r1) h = (g1 - b1) / d + (g1 < b1 ? 6 : 0);
87
- else if (max === g1) h = (b1 - r1) / d + 2;
88
- else h = (r1 - g1) / d + 4;
89
-
90
- return [(h * 60 + 360) % 360, s, l];
91
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Tracks requests per user and throws if the rate limit is exceeded.
3
- *
4
- * @param rateLimits - Store of timestamps per user
5
- * @param userId - Identifier of the requesting user
6
- * @param timestamp - Current timestamp in milliseconds
7
- * @param window - Time window in milliseconds
8
- * @param limit - Maximum requests allowed in the window
9
- * @returns false if within limits
10
- * @throws Error if rate limit is exceeded
11
- */
12
- export function checkRateLimit(
13
- rateLimits: Record<string, number[]>,
14
- userId: string,
15
- timestamp: number,
16
- window = 10000,
17
- limit = 50,
18
- ): boolean {
19
- rateLimits[userId] = (rateLimits[userId] ?? []).filter((ts) => timestamp - ts < window);
20
-
21
- if (rateLimits[userId]!.length > limit) {
22
- const remainingTime = Math.ceil((rateLimits[userId]![0]! + window - timestamp) / 1000);
23
- throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
24
- }
25
-
26
- rateLimits[userId]!.push(timestamp);
27
- return false;
28
- }
29
-
30
- /**
31
- * Parses a space-separated environment variable into an array.
32
- *
33
- * @param key - The environment variable name
34
- * @returns The parsed array, or null if not set
35
- */
36
- export function envList(key: string): string[] | null {
37
- const v = process.env[key];
38
- return v && v !== 'undefined' ? v.split(' ') : null;
39
- }
40
-
41
- /**
42
- * Formats a Date to ISO string without timezone suffix.
43
- *
44
- * @param date - The date to format
45
- * @returns ISO-formatted string without trailing Z
46
- */
47
- export function formatDate(date: Date): string {
48
- return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
49
- }
50
-
51
- /**
52
- * Generates a random IPv4 address.
53
- *
54
- * @returns A string in the format "X.X.X.X"
55
- */
56
- export function genIP(): string {
57
- return `${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`;
58
- }
59
-
60
- /**
61
- * Returns a random element from an array.
62
- *
63
- * @param arr - The source array
64
- * @returns A random element
65
- */
66
- export function random<T>(arr: T[]): T {
67
- return arr[Math.floor(Math.random() * arr.length)]!;
68
- }
69
-
70
- /**
71
- * Returns a random integer between min and max (inclusive).
72
- *
73
- * @param min - Lower bound
74
- * @param max - Upper bound
75
- * @returns A random integer in [min, max]
76
- */
77
- export function randomNumber(min: number, max: number): number {
78
- return Math.floor(Math.random() * (max - min + 1)) + min;
79
- }
80
-
81
- /**
82
- * Checks if a version string is at least the given minimum.
83
- *
84
- * @param version - Version string (e.g. "v4")
85
- * @param min - Minimum version number
86
- * @returns true if the version is >= min
87
- */
88
- export function since(version: string, min: number): boolean {
89
- return parseInt(version.replace('v', '')) >= min;
90
- }
@@ -1,28 +0,0 @@
1
- import type { Response } from 'express';
2
-
3
- import { DOCS_URL, STATUS_MESSAGES } from '../constants.js';
4
-
5
- /**
6
- * Sends a standardized JSON error response.
7
- *
8
- * @param res - Express response object
9
- * @param status - HTTP status code
10
- * @param message - Error description
11
- * @param docPath - Optional documentation path appended to the base URL
12
- */
13
- export function error(res: Response, status: number, message: string, docPath?: string): void {
14
- const body = {
15
- message: STATUS_MESSAGES[status] ?? 'Error',
16
- error: message,
17
- documentation: docPath ? `${DOCS_URL}/${docPath}` : DOCS_URL,
18
- status: String(status),
19
- };
20
-
21
- if (typeof res.jsonResponse === 'function') {
22
- res.status(status).jsonResponse(body);
23
- } else {
24
- res.status(status)
25
- .setHeader('Content-Type', 'application/json')
26
- .send(JSON.stringify(body, null, 2));
27
- }
28
- }