@20syldev/api 4.7.0 → 4.9.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 (191) hide show
  1. package/README.md +84 -47
  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/{src/modules/v4/address.ts → dist/modules/v4/address.js} +14 -46
  18. package/dist/modules/v4/address.js.map +1 -0
  19. package/{src/modules/v4/agent.ts → dist/modules/v4/agent.js} +26 -38
  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/{src/modules/v4/ip.ts → dist/modules/v4/ip.js} +58 -60
  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/{src/modules/v4/password.ts → dist/modules/v4/password.js} +25 -63
  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 +2 -1
  93. package/.github/FUNDING.yml +0 -1
  94. package/.prettierrc +0 -7
  95. package/eslint.config.js +0 -24
  96. package/robots.txt +0 -74
  97. package/src/app.ts +0 -53
  98. package/src/config/env.ts +0 -87
  99. package/src/config/plans.ts +0 -61
  100. package/src/config/versions.ts +0 -119
  101. package/src/constants.ts +0 -46
  102. package/src/favicon.ico +0 -0
  103. package/src/middleware/cors.ts +0 -23
  104. package/src/middleware/error.ts +0 -16
  105. package/src/middleware/json.ts +0 -9
  106. package/src/middleware/logger.ts +0 -32
  107. package/src/middleware/ratelimit.ts +0 -89
  108. package/src/middleware/version.ts +0 -40
  109. package/src/modules/v3/algorithms.js +0 -218
  110. package/src/modules/v3/captcha.js +0 -60
  111. package/src/modules/v3/chat.js +0 -111
  112. package/src/modules/v3/color.js +0 -60
  113. package/src/modules/v3/convert.js +0 -39
  114. package/src/modules/v3/domain.js +0 -86
  115. package/src/modules/v3/hash.js +0 -16
  116. package/src/modules/v3/hyperplanning.js +0 -56
  117. package/src/modules/v3/levenshtein.js +0 -46
  118. package/src/modules/v3/personal.js +0 -139
  119. package/src/modules/v3/qrcode.js +0 -20
  120. package/src/modules/v3/tic_tac_toe.js +0 -242
  121. package/src/modules/v3/time.js +0 -98
  122. package/src/modules/v3/token.js +0 -54
  123. package/src/modules/v3/username.js +0 -94
  124. package/src/modules/v3/utils.js +0 -77
  125. package/src/modules/v3.js +0 -15
  126. package/src/modules/v4/algorithms.ts +0 -276
  127. package/src/modules/v4/captcha.ts +0 -114
  128. package/src/modules/v4/chat.ts +0 -130
  129. package/src/modules/v4/color.ts +0 -75
  130. package/src/modules/v4/convert.ts +0 -127
  131. package/src/modules/v4/dice.ts +0 -46
  132. package/src/modules/v4/domain.ts +0 -86
  133. package/src/modules/v4/encode.ts +0 -241
  134. package/src/modules/v4/geo.ts +0 -63
  135. package/src/modules/v4/hash.ts +0 -32
  136. package/src/modules/v4/hyperplanning.ts +0 -93
  137. package/src/modules/v4/levenshtein.ts +0 -46
  138. package/src/modules/v4/palette.ts +0 -71
  139. package/src/modules/v4/personal.ts +0 -140
  140. package/src/modules/v4/placeholder.ts +0 -197
  141. package/src/modules/v4/qrcode.ts +0 -121
  142. package/src/modules/v4/statistics.ts +0 -62
  143. package/src/modules/v4/text.ts +0 -330
  144. package/src/modules/v4/tic_tac_toe.ts +0 -264
  145. package/src/modules/v4/time.ts +0 -103
  146. package/src/modules/v4/token.ts +0 -53
  147. package/src/modules/v4/username.ts +0 -94
  148. package/src/modules/v4/validate.ts +0 -76
  149. package/src/modules/v4.ts +0 -27
  150. package/src/routes/delete.ts +0 -72
  151. package/src/routes/get.ts +0 -835
  152. package/src/routes/index.ts +0 -59
  153. package/src/routes/patch.ts +0 -45
  154. package/src/routes/post.ts +0 -209
  155. package/src/storage/index.ts +0 -16
  156. package/src/types/express.d.ts +0 -17
  157. package/src/types/storage.ts +0 -48
  158. package/src/utils/colors.ts +0 -91
  159. package/src/utils/helpers.ts +0 -90
  160. package/src/utils/response.ts +0 -28
  161. package/tests/integration/api.test.ts +0 -716
  162. package/tests/tsconfig.json +0 -3
  163. package/tests/unit/address.test.ts +0 -84
  164. package/tests/unit/agent.test.ts +0 -113
  165. package/tests/unit/algorithms.test.ts +0 -121
  166. package/tests/unit/captcha.test.ts +0 -62
  167. package/tests/unit/chat.test.ts +0 -54
  168. package/tests/unit/color.test.ts +0 -54
  169. package/tests/unit/convert.test.ts +0 -68
  170. package/tests/unit/dice.test.ts +0 -58
  171. package/tests/unit/domain.test.ts +0 -48
  172. package/tests/unit/encode.test.ts +0 -109
  173. package/tests/unit/geo.test.ts +0 -46
  174. package/tests/unit/hash.test.ts +0 -45
  175. package/tests/unit/hyperplanning.test.ts +0 -97
  176. package/tests/unit/ip.test.ts +0 -140
  177. package/tests/unit/levenshtein.test.ts +0 -35
  178. package/tests/unit/palette.test.ts +0 -54
  179. package/tests/unit/password.test.ts +0 -86
  180. package/tests/unit/personal.test.ts +0 -66
  181. package/tests/unit/placeholder.test.ts +0 -58
  182. package/tests/unit/qrcode.test.ts +0 -83
  183. package/tests/unit/statistics.test.ts +0 -58
  184. package/tests/unit/text.test.ts +0 -108
  185. package/tests/unit/tic_tac_toe.test.ts +0 -56
  186. package/tests/unit/time.test.ts +0 -68
  187. package/tests/unit/token.test.ts +0 -62
  188. package/tests/unit/username.test.ts +0 -35
  189. package/tests/unit/validate.test.ts +0 -72
  190. package/tsconfig.json +0 -17
  191. 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
- }