@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,200 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+ import { chatStorage, ticTacToeStorage } from '../storage/index.js';
3
+ import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../constants.js';
4
+ import { error } from '../utils/response.js';
5
+
6
+ const router = Router();
7
+
8
+ // Store chat messages
9
+ router.post('/:version/chat', (req: Request, res: Response) => {
10
+ const { username, message, timestamp, session, token } = (req.body as Record<string, string>) || {};
11
+
12
+ if (!username) {
13
+ error(res, 400, 'Please provide a username (?username={username})');
14
+ return;
15
+ }
16
+ if (!message) {
17
+ error(res, 400, 'Please provide a message (&message={message})');
18
+ return;
19
+ }
20
+ if (!session) {
21
+ error(res, 400, 'Please provide a valid session ID (&session={ID})');
22
+ return;
23
+ }
24
+
25
+ try {
26
+ const result = req.module.chat('message', {
27
+ username,
28
+ message,
29
+ timestamp,
30
+ session,
31
+ token,
32
+ storage: chatStorage,
33
+ });
34
+ res.jsonResponse(result);
35
+ } catch (err) {
36
+ error(res, 400, (err as Error).message);
37
+ }
38
+ });
39
+
40
+ // Display a private chat with a token
41
+ router.post('/:version/chat/private', (req: Request, res: Response) => {
42
+ const { username, token } = (req.body as Record<string, string>) || {};
43
+
44
+ if (!username) {
45
+ error(res, 400, 'Please provide a username (?username={username})');
46
+ return;
47
+ }
48
+ if (!token) {
49
+ error(res, 400, 'Please provide a valid token (&token={key}).');
50
+ return;
51
+ }
52
+
53
+ try {
54
+ const messages = req.module.chat('private', {
55
+ username,
56
+ token,
57
+ storage: chatStorage,
58
+ });
59
+ res.jsonResponse(messages);
60
+ } catch (err) {
61
+ error(res, 400, (err as Error).message);
62
+ }
63
+ });
64
+
65
+ // Generate hash
66
+ router.post('/:version/hash', (req: Request, res: Response) => {
67
+ const { text, method } = (req.body as Record<string, string>) || {};
68
+
69
+ if (!text) {
70
+ error(res, 400, 'Please provide a text (?text={text})', `${req.version}/hash`);
71
+ return;
72
+ }
73
+ if (!method) {
74
+ error(res, 400, 'Please provide a valid hash algorithm (&method={algorithm})', `${req.version}/hash`);
75
+ return;
76
+ }
77
+
78
+ try {
79
+ const result = req.module.hash(text, method);
80
+ res.jsonResponse(result);
81
+ } catch (err) {
82
+ error(res, 400, (err as Error).message, `${req.version}/hash`);
83
+ }
84
+ });
85
+
86
+ // Display a planning from an ICS file
87
+ router.post('/:version/hyperplanning', async (req: Request, res: Response) => {
88
+ const { url, detail } = (req.body as Record<string, string>) || {};
89
+
90
+ if (!url) {
91
+ error(res, 400, 'Please provide a valid ICS file URL (?url={URL})', `${req.version}/hyperplanning`);
92
+ return;
93
+ }
94
+
95
+ try {
96
+ const hyperplanning = await req.module.hyperplanning(url, detail);
97
+ res.jsonResponse(hyperplanning);
98
+ } catch (err) {
99
+ error(res, 400, (err as Error).message, `${req.version}/hyperplanning`);
100
+ }
101
+ });
102
+
103
+ // Store tic tac toe games
104
+ router.post('/:version/tic-tac-toe', (req: Request, res: Response) => {
105
+ const { username, move, session, game } = (req.body as Record<string, string>) || {};
106
+
107
+ if (!username) {
108
+ error(res, 400, 'Please provide a username (?username={username})');
109
+ return;
110
+ }
111
+ if (!move) {
112
+ error(res, 400, 'Please provide a valid move (&move={move})');
113
+ return;
114
+ }
115
+ if (!session) {
116
+ error(res, 400, 'Please provide a valid session ID (&session={ID})');
117
+ return;
118
+ }
119
+ if (!game) {
120
+ error(res, 400, 'Please provide a valid game ID (&game={ID})');
121
+ return;
122
+ }
123
+
124
+ try {
125
+ const result = req.module.tic_tac_toe('play', {
126
+ username,
127
+ move,
128
+ session,
129
+ game,
130
+ storage: ticTacToeStorage,
131
+ });
132
+ res.jsonResponse(result);
133
+ } catch (err) {
134
+ error(res, 400, (err as Error).message);
135
+ }
136
+ });
137
+
138
+ // Display a tic tac toe game with a token
139
+ router.post('/:version/tic-tac-toe/fetch', (req: Request, res: Response) => {
140
+ const { username, game } = (req.body as Record<string, string>) || {};
141
+ const privateGame = ((req.body as Record<string, unknown>) || {}).private as boolean | undefined;
142
+
143
+ if (!username) {
144
+ error(res, 400, 'Please provide a username (?username={username})');
145
+ return;
146
+ }
147
+
148
+ try {
149
+ const result = req.module.tic_tac_toe('fetch', {
150
+ username,
151
+ game,
152
+ private: privateGame,
153
+ storage: ticTacToeStorage,
154
+ });
155
+ res.jsonResponse(result);
156
+ } catch (err) {
157
+ error(res, 400, (err as Error).message);
158
+ }
159
+ });
160
+
161
+ // List public tic tac toe games
162
+ router.post('/:version/tic-tac-toe/list', (req: Request, res: Response) => {
163
+ try {
164
+ const result = req.module.tic_tac_toe('list', {
165
+ storage: ticTacToeStorage,
166
+ });
167
+ res.jsonResponse(result);
168
+ } catch (err) {
169
+ error(res, 400, (err as Error).message);
170
+ }
171
+ });
172
+
173
+ // Generate Token
174
+ router.post('/:version/token', (req: Request, res: Response) => {
175
+ const body = (req.body as Record<string, unknown>) || {};
176
+ const len = parseInt(String(body.len || 24), 10);
177
+ const type = body.type ? String(body.type).toLowerCase() : 'alpha';
178
+
179
+ if (isNaN(len) || len < 0) {
180
+ error(res, 400, 'Invalid number.', `${req.version}/token`);
181
+ return;
182
+ }
183
+ if (len > MAX_TOKEN_LENGTH) {
184
+ error(res, 400, 'Length cannot exceed 4096.', `${req.version}/token`);
185
+ return;
186
+ }
187
+ if (len < MIN_TOKEN_LENGTH) {
188
+ error(res, 400, 'Length cannot be less than 12.', `${req.version}/token`);
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const token = req.module.token(len, type);
194
+ res.jsonResponse({ token });
195
+ } catch (err) {
196
+ error(res, 400, (err as Error).message, `${req.version}/token`);
197
+ }
198
+ });
199
+
200
+ export default router;
@@ -0,0 +1,16 @@
1
+ import type { ChatStorage, TicTacToeStorage, IpLimits } 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
+ };
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ import type { Response } from 'express';
2
+ import { DOCS_URL, STATUS_MESSAGES } from '../constants.js';
3
+
4
+ export function error(res: Response, status: number, message: string, docPath?: string): void {
5
+ res.status(status).jsonResponse({
6
+ message: STATUS_MESSAGES[status] ?? 'Error',
7
+ error: message,
8
+ documentation: docPath ? `${DOCS_URL}/${docPath}` : DOCS_URL,
9
+ status: String(status),
10
+ });
11
+ }
@@ -0,0 +1,5 @@
1
+ import type { Request } from 'express';
2
+
3
+ const REST_VERSIONS = new Set(['v4']);
4
+
5
+ export const supportsRest = (req: Request): boolean => REST_VERSIONS.has(req.version);
@@ -0,0 +1,395 @@
1
+ import { test, describe, before, after } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import type { AddressInfo } from 'node:net';
4
+ import type { Server } from 'node:http';
5
+ import app from '../../src/app.js';
6
+
7
+ let server: Server;
8
+ let baseUrl: string;
9
+
10
+ before(() => {
11
+ return new Promise<void>((resolve) => {
12
+ server = app.listen(0, () => {
13
+ const port = (server.address() as AddressInfo).port;
14
+ baseUrl = `http://127.0.0.1:${port}`;
15
+ resolve();
16
+ });
17
+ });
18
+ });
19
+
20
+ after(() => {
21
+ return new Promise<void>((resolve) => {
22
+ server.close(() => resolve());
23
+ });
24
+ });
25
+
26
+ async function getJson(path: string): Promise<{ status: number; body: Record<string, unknown> }> {
27
+ const res = await fetch(`${baseUrl}${path}`);
28
+ const body = (await res.json()) as Record<string, unknown>;
29
+ return { status: res.status, body };
30
+ }
31
+
32
+ async function sendJson(
33
+ method: string,
34
+ path: string,
35
+ body: Record<string, unknown>,
36
+ ): Promise<{ status: number; body: Record<string, unknown> }> {
37
+ const res = await fetch(`${baseUrl}${path}`, {
38
+ method,
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(body),
41
+ });
42
+ const data = (await res.json()) as Record<string, unknown>;
43
+ return { status: res.status, body: data };
44
+ }
45
+
46
+ // --- GET endpoints ---
47
+
48
+ describe('GET / (version listing)', () => {
49
+ test('v4 endpoints listed', async () => {
50
+ const { body } = await getJson('/v4');
51
+ assert.equal(body.version, 'v4');
52
+ const endpoints = body.endpoints as Record<string, Record<string, string>>;
53
+ assert.ok('dice' in endpoints.get!);
54
+ assert.ok('statistics' in endpoints.get!);
55
+ });
56
+
57
+ test('invalid version returns 404', async () => {
58
+ const { status } = await getJson('/v99');
59
+ assert.equal(status, 404);
60
+ });
61
+ });
62
+
63
+ describe('GET /v4/algorithms', () => {
64
+ test('fibonacci', async () => {
65
+ const { status, body } = await getJson('/v4/algorithms?method=fibonacci&value=10');
66
+ assert.equal(status, 200);
67
+ assert.deepEqual(body.answer, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
68
+ });
69
+
70
+ test('isprime', async () => {
71
+ const { body } = await getJson('/v4/algorithms?method=isprime&value=17');
72
+ assert.equal(body.answer, true);
73
+ });
74
+
75
+ test('roman 2024 to MMXXIV', async () => {
76
+ const { body } = await getJson('/v4/algorithms?method=roman&value=2024');
77
+ assert.equal(body.answer, 'MMXXIV');
78
+ });
79
+
80
+ test('roman MMXXIV to 2024', async () => {
81
+ const { body } = await getJson('/v4/algorithms?method=roman&value=MMXXIV');
82
+ assert.equal(body.answer, 2024);
83
+ });
84
+ });
85
+
86
+ describe('GET /v4/captcha', () => {
87
+ test('returns PNG', async () => {
88
+ const res = await fetch(`${baseUrl}/v4/captcha?text=hello`);
89
+ assert.equal(res.status, 200);
90
+ assert.match(res.headers.get('content-type') ?? '', /image\/png/);
91
+ });
92
+
93
+ test('without text returns 400', async () => {
94
+ const { status } = await getJson('/v4/captcha');
95
+ assert.equal(status, 400);
96
+ });
97
+ });
98
+
99
+ describe('GET /v4/chat', () => {
100
+ test('fetches public messages', async () => {
101
+ await sendJson('POST', '/v4/chat', {
102
+ username: 'get-chat-user',
103
+ message: 'ping',
104
+ session: `get-chat-${Date.now()}`,
105
+ });
106
+ const res = await fetch(`${baseUrl}/v4/chat`);
107
+ assert.equal(res.status, 200);
108
+ const body = (await res.json()) as { username: string; message: string }[];
109
+ assert.ok(Array.isArray(body));
110
+ });
111
+ });
112
+
113
+ describe('GET /v4/color', () => {
114
+ test('returns color formats', async () => {
115
+ const { status, body } = await getJson('/v4/color');
116
+ assert.equal(status, 200);
117
+ assert.match(body.hex as string, /^#[0-9a-f]{6}$/);
118
+ assert.ok('rgb' in body);
119
+ assert.ok('hsl' in body);
120
+ assert.ok('cmyk' in body);
121
+ });
122
+ });
123
+
124
+ describe('GET /v4/convert', () => {
125
+ test('celsius to fahrenheit', async () => {
126
+ const { status, body } = await getJson('/v4/convert?value=100&from=celsius&to=fahrenheit');
127
+ assert.equal(status, 200);
128
+ assert.equal(body.result, 212);
129
+ });
130
+
131
+ test('missing value returns 400', async () => {
132
+ const { status } = await getJson('/v4/convert?from=celsius&to=fahrenheit');
133
+ assert.equal(status, 400);
134
+ });
135
+ });
136
+
137
+ describe('GET /v4/dice', () => {
138
+ test('rolls 2d6+3', async () => {
139
+ const { body } = await getJson('/v4/dice?roll=2d6%2B3');
140
+ assert.equal(body.count, 2);
141
+ assert.equal(body.sides, 6);
142
+ assert.equal(body.modifier, 3);
143
+ });
144
+
145
+ test('rejects invalid notation', async () => {
146
+ const { status } = await getJson('/v4/dice?roll=foo');
147
+ assert.equal(status, 400);
148
+ });
149
+ });
150
+
151
+ describe('GET /v4/domain', () => {
152
+ test('has TLD', async () => {
153
+ const { status, body } = await getJson('/v4/domain');
154
+ assert.equal(status, 200);
155
+ assert.match(body.domain as string, /\./);
156
+ });
157
+ });
158
+
159
+ describe('GET /v4/infos', () => {
160
+ test('returns API infos', async () => {
161
+ const { status, body } = await getJson('/v4/infos');
162
+ assert.equal(status, 200);
163
+ assert.ok('endpoints' in body);
164
+ assert.ok('documentation' in body);
165
+ });
166
+ });
167
+
168
+ describe('GET /v4/levenshtein', () => {
169
+ test('computes distance', async () => {
170
+ const { status, body } = await getJson('/v4/levenshtein?str1=kitten&str2=sitting');
171
+ assert.equal(status, 200);
172
+ assert.equal(body.distance, 3);
173
+ });
174
+ });
175
+
176
+ describe('GET /v4/personal', () => {
177
+ test('returns generated identity', async () => {
178
+ const { status, body } = await getJson('/v4/personal');
179
+ assert.equal(status, 200);
180
+ assert.ok('name' in body);
181
+ assert.ok('email' in body);
182
+ assert.ok('card' in body);
183
+ });
184
+ });
185
+
186
+ describe('GET /v4/qrcode', () => {
187
+ test('returns data URL', async () => {
188
+ const { status, body } = await getJson('/v4/qrcode?url=https%3A%2F%2Fexample.com');
189
+ assert.equal(status, 200);
190
+ assert.match(body as unknown as string, /^data:image\/png;base64,/);
191
+ });
192
+ });
193
+
194
+ describe('GET /v4/statistics', () => {
195
+ test('basic stats', async () => {
196
+ const { body } = await getJson('/v4/statistics?values=1,2,3,4,5');
197
+ assert.equal(body.mean, 3);
198
+ assert.equal(body.median, 3);
199
+ assert.equal(body.count, 5);
200
+ });
201
+
202
+ test('missing values returns 400', async () => {
203
+ const { status } = await getJson('/v4/statistics');
204
+ assert.equal(status, 400);
205
+ });
206
+ });
207
+
208
+ describe('GET /v4/time', () => {
209
+ test('live', async () => {
210
+ const { status, body } = await getJson('/v4/time');
211
+ assert.equal(status, 200);
212
+ assert.ok('iso' in body);
213
+ assert.ok('timestamp' in body);
214
+ });
215
+
216
+ test('random in range', async () => {
217
+ const { status, body } = await getJson('/v4/time?type=random&start=2020-01-01&end=2020-12-31');
218
+ assert.equal(status, 200);
219
+ const ts = body.timestamp as number;
220
+ assert.ok(ts >= new Date('2020-01-01').getTime());
221
+ assert.ok(ts <= new Date('2020-12-31').getTime());
222
+ });
223
+ });
224
+
225
+ describe('GET /v4/username', () => {
226
+ test('has fields', async () => {
227
+ const { status, body } = await getJson('/v4/username');
228
+ assert.equal(status, 200);
229
+ assert.ok('username' in body);
230
+ assert.ok('adjective' in body);
231
+ assert.ok('animal' in body);
232
+ });
233
+ });
234
+
235
+ describe('GET /v4/website', () => {
236
+ const originalFetch = globalThis.fetch;
237
+
238
+ after(() => {
239
+ globalThis.fetch = originalFetch;
240
+ });
241
+
242
+ test('returns aggregated payload (with mocked GitHub API)', async () => {
243
+ globalThis.fetch = (async (input: string | URL | Request) => {
244
+ const url = typeof input === 'string' ? input : input.toString();
245
+ if (url.includes('api.github.com')) {
246
+ return new Response(
247
+ JSON.stringify({
248
+ data: {
249
+ user: {
250
+ contributionsCollection: {
251
+ contributionCalendar: {
252
+ weeks: [
253
+ {
254
+ firstDay: '2026-01-01',
255
+ contributionDays: [{ date: '2026-01-01', contributionCount: 5 }],
256
+ },
257
+ ],
258
+ },
259
+ },
260
+ },
261
+ },
262
+ }),
263
+ { status: 200, headers: { 'content-type': 'application/json' } },
264
+ );
265
+ }
266
+ return originalFetch(input);
267
+ }) as typeof fetch;
268
+
269
+ const { status, body } = await getJson('/v4/website');
270
+ assert.equal(status, 200);
271
+ assert.ok('versions' in body);
272
+ assert.ok('stats' in body);
273
+ });
274
+
275
+ test('?key=stats.5 returns sub-key', async () => {
276
+ const { status, body } = await getJson('/v4/website?key=stats.5');
277
+ assert.equal(status, 200);
278
+ assert.ok('stats.5' in body);
279
+ });
280
+
281
+ test('?key=invalid.path returns 404', async () => {
282
+ const { status } = await getJson('/v4/website?key=does.not.exist');
283
+ assert.equal(status, 404);
284
+ });
285
+
286
+ test('rejects prototype pollution keys', async () => {
287
+ const { status } = await getJson('/v4/website?key=__proto__');
288
+ assert.equal(status, 400);
289
+ });
290
+ });
291
+
292
+ // --- POST endpoints ---
293
+
294
+ describe('POST /v4/chat', () => {
295
+ const session = `chat-${Date.now()}`;
296
+
297
+ test('sends a public message', async () => {
298
+ const { status, body } = await sendJson('POST', '/v4/chat', {
299
+ username: 'alice',
300
+ message: 'public hello',
301
+ session,
302
+ });
303
+ assert.equal(status, 200);
304
+ assert.match(body.message as string, /sent/);
305
+ });
306
+
307
+ test('fetches private chat (legacy)', async () => {
308
+ const token = `tok-${Date.now()}`;
309
+ await sendJson('POST', '/v4/chat', {
310
+ username: 'bob',
311
+ message: 'secret',
312
+ session: `chat-bob-${Date.now()}`,
313
+ token,
314
+ });
315
+ const { status, body } = await sendJson('POST', '/v4/chat/private', {
316
+ username: 'bob',
317
+ token,
318
+ });
319
+ assert.equal(status, 200);
320
+ assert.ok(Array.isArray(body));
321
+ });
322
+ });
323
+
324
+ describe('POST /v4/hash', () => {
325
+ test('sha256', async () => {
326
+ const { status, body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'sha256' });
327
+ assert.equal(status, 200);
328
+ assert.equal(body.hash, '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
329
+ });
330
+
331
+ test('md5', async () => {
332
+ const { body } = await sendJson('POST', '/v4/hash', { text: 'hello', method: 'md5' });
333
+ assert.equal(body.hash, '5d41402abc4b2a76b9719d911017c592');
334
+ });
335
+
336
+ test('missing text returns 400', async () => {
337
+ const { status } = await sendJson('POST', '/v4/hash', { method: 'sha256' });
338
+ assert.equal(status, 400);
339
+ });
340
+ });
341
+
342
+ describe('POST /v4/tic-tac-toe', () => {
343
+ const game = 'TTT' + Date.now().toString(36).slice(-3).toUpperCase();
344
+ const session = `ttt-${Date.now()}`;
345
+
346
+ test('list returns games', async () => {
347
+ const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/list', {});
348
+ assert.equal(status, 200);
349
+ assert.ok(Array.isArray(body.games));
350
+ });
351
+
352
+ test('plays a move', async () => {
353
+ const { status, body } = await sendJson('POST', '/v4/tic-tac-toe', {
354
+ username: 'alice',
355
+ move: '1-1',
356
+ session,
357
+ game,
358
+ });
359
+ assert.equal(status, 200);
360
+ assert.match(body.message as string, /Move sent/);
361
+ });
362
+
363
+ test('fetch returns game state', async () => {
364
+ const { status, body } = await sendJson('POST', '/v4/tic-tac-toe/fetch', {
365
+ username: 'alice',
366
+ game,
367
+ });
368
+ assert.equal(status, 200);
369
+ assert.equal(body.id, game);
370
+ });
371
+
372
+ test('missing move returns 400', async () => {
373
+ const { status } = await sendJson('POST', '/v4/tic-tac-toe', {
374
+ username: 'alice',
375
+ session,
376
+ game,
377
+ });
378
+ assert.equal(status, 400);
379
+ });
380
+ });
381
+
382
+ describe('POST /v4/token', () => {
383
+ test('alpha 24', async () => {
384
+ const { status, body } = await sendJson('POST', '/v4/token', { len: 24, type: 'alpha' });
385
+ assert.equal(status, 200);
386
+ assert.equal(typeof body.token, 'string');
387
+ assert.equal((body.token as string).length, 24);
388
+ assert.match(body.token as string, /^[a-zA-Z]+$/);
389
+ });
390
+
391
+ test('below min length returns 400', async () => {
392
+ const { status } = await sendJson('POST', '/v4/token', { len: 5, type: 'alpha' });
393
+ assert.equal(status, 400);
394
+ });
395
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../tsconfig.test.json"
3
+ }