@20syldev/api 4.8.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 (109) hide show
  1. package/README.md +84 -47
  2. package/package.json +2 -1
  3. package/.github/FUNDING.yml +0 -1
  4. package/.prettierrc +0 -7
  5. package/eslint.config.js +0 -24
  6. package/robots.txt +0 -74
  7. package/src/app.ts +0 -53
  8. package/src/config/env.ts +0 -87
  9. package/src/config/plans.ts +0 -61
  10. package/src/config/versions.ts +0 -121
  11. package/src/constants.ts +0 -61
  12. package/src/favicon.ico +0 -0
  13. package/src/middleware/cors.ts +0 -23
  14. package/src/middleware/error.ts +0 -16
  15. package/src/middleware/json.ts +0 -9
  16. package/src/middleware/logger.ts +0 -32
  17. package/src/middleware/ratelimit.ts +0 -89
  18. package/src/middleware/version.ts +0 -40
  19. package/src/modules/v3/algorithms.js +0 -218
  20. package/src/modules/v3/captcha.js +0 -60
  21. package/src/modules/v3/chat.js +0 -111
  22. package/src/modules/v3/color.js +0 -60
  23. package/src/modules/v3/convert.js +0 -39
  24. package/src/modules/v3/domain.js +0 -86
  25. package/src/modules/v3/hash.js +0 -16
  26. package/src/modules/v3/hyperplanning.js +0 -56
  27. package/src/modules/v3/levenshtein.js +0 -46
  28. package/src/modules/v3/personal.js +0 -139
  29. package/src/modules/v3/qrcode.js +0 -20
  30. package/src/modules/v3/tic_tac_toe.js +0 -242
  31. package/src/modules/v3/time.js +0 -98
  32. package/src/modules/v3/token.js +0 -54
  33. package/src/modules/v3/username.js +0 -94
  34. package/src/modules/v3/utils.js +0 -77
  35. package/src/modules/v3.js +0 -15
  36. package/src/modules/v4/address.ts +0 -412
  37. package/src/modules/v4/agent.ts +0 -104
  38. package/src/modules/v4/algorithms.ts +0 -276
  39. package/src/modules/v4/captcha.ts +0 -114
  40. package/src/modules/v4/chat.ts +0 -130
  41. package/src/modules/v4/color.ts +0 -75
  42. package/src/modules/v4/convert.ts +0 -127
  43. package/src/modules/v4/cron.ts +0 -221
  44. package/src/modules/v4/dice.ts +0 -46
  45. package/src/modules/v4/domain.ts +0 -86
  46. package/src/modules/v4/encode.ts +0 -241
  47. package/src/modules/v4/geo.ts +0 -64
  48. package/src/modules/v4/hash.ts +0 -32
  49. package/src/modules/v4/hyperplanning.ts +0 -93
  50. package/src/modules/v4/ip.ts +0 -134
  51. package/src/modules/v4/levenshtein.ts +0 -46
  52. package/src/modules/v4/palette.ts +0 -71
  53. package/src/modules/v4/password.ts +0 -379
  54. package/src/modules/v4/personal.ts +0 -140
  55. package/src/modules/v4/placeholder.ts +0 -197
  56. package/src/modules/v4/qrcode.ts +0 -121
  57. package/src/modules/v4/regex.ts +0 -67
  58. package/src/modules/v4/statistics.ts +0 -62
  59. package/src/modules/v4/text.ts +0 -330
  60. package/src/modules/v4/tic_tac_toe.ts +0 -264
  61. package/src/modules/v4/time.ts +0 -149
  62. package/src/modules/v4/token.ts +0 -53
  63. package/src/modules/v4/username.ts +0 -94
  64. package/src/modules/v4/validate.ts +0 -76
  65. package/src/modules/v4.ts +0 -29
  66. package/src/routes/delete.ts +0 -72
  67. package/src/routes/get.ts +0 -856
  68. package/src/routes/index.ts +0 -59
  69. package/src/routes/patch.ts +0 -45
  70. package/src/routes/post.ts +0 -209
  71. package/src/storage/index.ts +0 -16
  72. package/src/types/express.d.ts +0 -17
  73. package/src/types/storage.ts +0 -48
  74. package/src/utils/colors.ts +0 -91
  75. package/src/utils/helpers.ts +0 -90
  76. package/src/utils/response.ts +0 -28
  77. package/tests/integration/api.test.ts +0 -772
  78. package/tests/tsconfig.json +0 -3
  79. package/tests/unit/address.test.ts +0 -84
  80. package/tests/unit/agent.test.ts +0 -113
  81. package/tests/unit/algorithms.test.ts +0 -121
  82. package/tests/unit/captcha.test.ts +0 -62
  83. package/tests/unit/chat.test.ts +0 -54
  84. package/tests/unit/color.test.ts +0 -54
  85. package/tests/unit/convert.test.ts +0 -68
  86. package/tests/unit/cron.test.ts +0 -110
  87. package/tests/unit/dice.test.ts +0 -58
  88. package/tests/unit/domain.test.ts +0 -48
  89. package/tests/unit/encode.test.ts +0 -109
  90. package/tests/unit/geo.test.ts +0 -46
  91. package/tests/unit/hash.test.ts +0 -45
  92. package/tests/unit/hyperplanning.test.ts +0 -97
  93. package/tests/unit/ip.test.ts +0 -140
  94. package/tests/unit/levenshtein.test.ts +0 -35
  95. package/tests/unit/palette.test.ts +0 -54
  96. package/tests/unit/password.test.ts +0 -86
  97. package/tests/unit/personal.test.ts +0 -66
  98. package/tests/unit/placeholder.test.ts +0 -58
  99. package/tests/unit/qrcode.test.ts +0 -83
  100. package/tests/unit/regex.test.ts +0 -95
  101. package/tests/unit/statistics.test.ts +0 -58
  102. package/tests/unit/text.test.ts +0 -108
  103. package/tests/unit/tic_tac_toe.test.ts +0 -56
  104. package/tests/unit/time.test.ts +0 -115
  105. package/tests/unit/token.test.ts +0 -62
  106. package/tests/unit/username.test.ts +0 -35
  107. package/tests/unit/validate.test.ts +0 -72
  108. package/tsconfig.json +0 -17
  109. package/tsconfig.test.json +0 -9
@@ -1,149 +0,0 @@
1
- import { MAX_COUNTDOWN_YEARS } from '../../constants.js';
2
-
3
- const VALID_FORMATS = [
4
- 'iso',
5
- 'utc',
6
- 'timestamp',
7
- 'locale',
8
- 'date',
9
- 'time',
10
- 'year',
11
- 'month',
12
- 'day',
13
- 'hour',
14
- 'minute',
15
- 'second',
16
- 'ms',
17
- 'dayOfWeek',
18
- 'dayOfYear',
19
- 'weekNumber',
20
- 'timezone',
21
- 'timezoneOffset',
22
- ] as const;
23
-
24
- export const VALID_TIMEZONES = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'] as const;
25
-
26
- type TimeFormat = (typeof VALID_FORMATS)[number];
27
- type Timezone = (typeof VALID_TIMEZONES)[number];
28
-
29
- /**
30
- * Returns the current or a random date/time in various formats and timezones,
31
- * or computes a countdown/elapsed time to/from a target date.
32
- *
33
- * @param type - "live" for the current time, "random" for a random date, or "countdown" for a time diff
34
- * @param start - Optional start date for random mode (YYYY-MM-DD)
35
- * @param end - Optional end date for random mode (YYYY-MM-DD)
36
- * @param format - Optional specific format to return (e.g. "iso", "timestamp", "year")
37
- * @param timezone - Optional timezone (e.g. "UTC", "Europe/Paris")
38
- * @param target - Required for countdown mode: target date in ISO 8601 or YYYY-MM-DD format
39
- * @returns Object containing all time formats, a single format, or a countdown result
40
- * @throws Error if type, format, timezone, or target is invalid
41
- */
42
- export default function time(
43
- type: string = 'live',
44
- start?: string,
45
- end?: string,
46
- format?: string,
47
- timezone?: string,
48
- target?: string,
49
- ): Record<string, unknown> {
50
- if (type !== 'live' && type !== 'random' && type !== 'countdown') {
51
- throw new Error('Please provide a valid type (live, random or countdown)');
52
- }
53
-
54
- if (timezone && !VALID_TIMEZONES.includes(timezone as Timezone)) {
55
- throw new Error(`Please provide a valid timezone. Options: ${VALID_TIMEZONES.join(', ')}`);
56
- }
57
-
58
- if (type === 'countdown') {
59
- if (!target) {
60
- throw new Error('Please provide a target date (?target=YYYY-MM-DD)');
61
- }
62
-
63
- const targetDate = new Date(target);
64
- if (isNaN(targetDate.getTime())) {
65
- throw new Error('Please provide a valid target date (ISO 8601 or YYYY-MM-DD)');
66
- }
67
-
68
- const now = new Date();
69
- const maxMs = MAX_COUNTDOWN_YEARS * 365.25 * 24 * 3600 * 1000;
70
-
71
- if (Math.abs(targetDate.getTime() - now.getTime()) > maxMs) {
72
- throw new Error(`Target date must be within ${MAX_COUNTDOWN_YEARS} years from now`);
73
- }
74
-
75
- const direction: 'future' | 'past' = targetDate > now ? 'future' : 'past';
76
- const diffMs = Math.abs(targetDate.getTime() - now.getTime());
77
- const total_seconds = Math.floor(diffMs / 1000);
78
-
79
- const days = Math.floor(total_seconds / 86400);
80
- const hours = Math.floor((total_seconds % 86400) / 3600);
81
- const minutes = Math.floor((total_seconds % 3600) / 60);
82
- const seconds = total_seconds % 60;
83
-
84
- const parts: string[] = [];
85
- if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`);
86
- if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? 's' : ''}`);
87
- if (minutes > 0) parts.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`);
88
- if (seconds > 0) parts.push(`${seconds} second${seconds !== 1 ? 's' : ''}`);
89
-
90
- return {
91
- target: targetDate.toISOString(),
92
- now: now.toISOString(),
93
- direction,
94
- remaining: { total_seconds, days, hours, minutes, seconds },
95
- human: parts.length > 0 ? parts.join(', ') : '0 seconds',
96
- };
97
- }
98
-
99
- if (start && !Date.parse(start)) {
100
- throw new Error('Please provide a valid start date (YYYY-MM-DD)');
101
- }
102
-
103
- if (end && !Date.parse(end)) {
104
- throw new Error('Please provide a valid end date (YYYY-MM-DD)');
105
- }
106
-
107
- if (format && !VALID_FORMATS.includes(format as TimeFormat)) {
108
- throw new Error(`Please provide a valid format. Options: ${VALID_FORMATS.join(', ')}`);
109
- }
110
-
111
- const getTimeFormats = (date: Date, tz: string): Record<string, unknown> => {
112
- return {
113
- iso: date.toISOString(),
114
- utc: date.toUTCString(),
115
- timestamp: date.getTime(),
116
- locale: date.toLocaleString('en-US', { timeZone: tz, timeZoneName: 'long' }),
117
- date: date.toLocaleDateString('en-US', { timeZone: tz }),
118
- time: date.toLocaleTimeString('en-US', { timeZone: tz }),
119
- year: date.getFullYear(),
120
- month: date.getMonth() + 1,
121
- day: date.getDate(),
122
- hour: date.getHours(),
123
- minute: date.getMinutes(),
124
- second: date.getSeconds(),
125
- ms: date.getMilliseconds(),
126
- dayOfWeek: date.getDay(),
127
- dayOfYear: Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000),
128
- weekNumber: Math.ceil(((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 + 1) / 7),
129
- timezone: tz,
130
- timezoneOffset: date.getTimezoneOffset(),
131
- };
132
- };
133
-
134
- if (type === 'random') {
135
- const startDate = start ? new Date(start).getTime() : new Date('1900-01-01').getTime();
136
- const endDate = end ? new Date(end).getTime() : new Date('2100-12-31').getTime();
137
- const randomDate = new Date(startDate + Math.random() * (endDate - startDate));
138
- const tz = timezone || VALID_TIMEZONES[Math.floor(Math.random() * VALID_TIMEZONES.length)]!;
139
- const formats = getTimeFormats(randomDate, tz);
140
-
141
- return format ? { date: formats[format] } : formats;
142
- }
143
-
144
- const now = new Date();
145
- const tz = timezone || 'UTC';
146
- const formats = getTimeFormats(now, tz);
147
-
148
- return format ? { date: formats[format] } : formats;
149
- }
@@ -1,53 +0,0 @@
1
- import { randomBytes, randomInt } from 'crypto';
2
- import { v4 } from 'uuid';
3
-
4
- import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../../constants.js';
5
-
6
- /**
7
- * Generates a cryptographically random token of the specified length and type.
8
- *
9
- * @param len - Token length (must be between 12 and 4096)
10
- * @param type - Character set to use: "alpha", "alphanum", "base64", "hex", "num", "punct", "urlsafe", or "uuid"
11
- * @returns The generated token string
12
- * @throws Error if length is out of range or the type is not valid
13
- */
14
- export default function token(len: number, type: string = 'alphanum'): string {
15
- if (isNaN(len) || len < MIN_TOKEN_LENGTH) {
16
- throw new Error('Length must be a number greater than or equal to 12');
17
- }
18
-
19
- if (len > MAX_TOKEN_LENGTH) {
20
- throw new Error('Length cannot exceed 4096');
21
- }
22
-
23
- const genToken = (chars: string, length: number): string =>
24
- Array.from({ length }, () => chars[randomInt(chars.length)]).join('');
25
-
26
- const tokenTypes: Record<string, () => string> = {
27
- alpha: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
28
- alphanum: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len),
29
- base64: () =>
30
- randomBytes(Math.ceil(len * 0.75))
31
- .toString('base64')
32
- .slice(0, len),
33
- hex: () =>
34
- randomBytes(Math.ceil(len * 0.5))
35
- .toString('hex')
36
- .slice(0, len),
37
- num: () => genToken('0123456789', len),
38
- punct: () => genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
39
- urlsafe: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
40
- uuid: () => {
41
- let result = '';
42
- while (result.length < len) result += v4().replace(/-/g, '');
43
- return result.slice(0, len);
44
- },
45
- };
46
-
47
- const lowerType = type.toLowerCase();
48
- if (!tokenTypes[lowerType]) {
49
- throw new Error(`Invalid token type. Valid types: ${Object.keys(tokenTypes).join(', ')}`);
50
- }
51
-
52
- return tokenTypes[lowerType]!();
53
- }
@@ -1,94 +0,0 @@
1
- import { random, randomNumber } from '../../utils/helpers.js';
2
-
3
- /**
4
- * Generates a random username combining adjectives, animals, and job titles.
5
- *
6
- * @returns Object containing the generated username, the number used, and the components (adjective, animal, job)
7
- */
8
- export default function username(): Record<string, unknown> {
9
- const adj = [
10
- 'Happy',
11
- 'Silly',
12
- 'Clever',
13
- 'Creative',
14
- 'Brave',
15
- 'Gentle',
16
- 'Kind',
17
- 'Funny',
18
- 'Wise',
19
- 'Charming',
20
- 'Sincere',
21
- 'Resourceful',
22
- 'Patient',
23
- 'Energetic',
24
- 'Adventurous',
25
- 'Ambitious',
26
- 'Courageous',
27
- 'Courteous',
28
- 'Determined',
29
- ];
30
- const ani = [
31
- 'Cat',
32
- 'Dog',
33
- 'Tiger',
34
- 'Elephant',
35
- 'Monkey',
36
- 'Penguin',
37
- 'Dolphin',
38
- 'Lion',
39
- 'Bear',
40
- 'Fox',
41
- 'Owl',
42
- 'Giraffe',
43
- 'Zebra',
44
- 'Koala',
45
- 'Rabbit',
46
- 'Squirrel',
47
- 'Panda',
48
- 'Horse',
49
- 'Wolf',
50
- 'Eagle',
51
- ];
52
- const job = [
53
- 'Writer',
54
- 'Artist',
55
- 'Musician',
56
- 'Explorer',
57
- 'Scientist',
58
- 'Engineer',
59
- 'Athlete',
60
- 'Chef',
61
- 'Doctor',
62
- 'Teacher',
63
- 'Lawyer',
64
- 'Entrepreneur',
65
- 'Actor',
66
- 'Dancer',
67
- 'Photographer',
68
- 'Architect',
69
- 'Pilot',
70
- 'Designer',
71
- 'Journalist',
72
- 'Veterinarian',
73
- ];
74
-
75
- const nombre = randomNumber(0, 99);
76
- const choix: Record<string, () => string> = {
77
- adj_num: () => random(adj) + nombre,
78
- ani_num: () => random(ani) + nombre,
79
- pro_num: () => random(job) + nombre,
80
- adj_ani: () => random(adj) + random(ani),
81
- adj_ani_num: () => random(adj) + random(ani) + nombre,
82
- adj_pro: () => random(adj) + random(job),
83
- pro_ani: () => random(job) + random(ani),
84
- pro_ani_num: () => random(job) + random(ani) + nombre,
85
- };
86
-
87
- return {
88
- username: choix[random(Object.keys(choix))]!(),
89
- number: nombre,
90
- adjective: random(adj),
91
- animal: random(ani),
92
- job: random(job),
93
- };
94
- }
@@ -1,76 +0,0 @@
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
- /**
11
- * Validates a credit/debit card number using the Luhn algorithm.
12
- *
13
- * @param value - Card number string (digits, spaces, or dashes allowed)
14
- * @returns Object containing the validity result and the sanitized digit string
15
- * @throws Error if value is missing, contains non-digit characters, or has an invalid length
16
- */
17
- export function luhn(value: string): { valid: boolean; value: string } {
18
- checkValue(value);
19
- const digits = value.replace(/\s|-/g, '');
20
- if (!/^\d+$/.test(digits)) throw new Error('Value must contain only digits, spaces or dashes');
21
- if (digits.length < 12 || digits.length > 19) throw new Error('Card number must have between 12 and 19 digits');
22
-
23
- let sum = 0;
24
- let alt = false;
25
- for (let i = digits.length - 1; i >= 0; i--) {
26
- let n = parseInt(digits[i]!, 10);
27
- if (alt) {
28
- n *= 2;
29
- if (n > 9) n -= 9;
30
- }
31
- sum += n;
32
- alt = !alt;
33
- }
34
-
35
- return { valid: sum % 10 === 0, value: digits };
36
- }
37
-
38
- /**
39
- * Validates an IBAN using the mod-97 checksum algorithm.
40
- *
41
- * @param value - IBAN string (spaces allowed)
42
- * @returns Object containing the validity result, sanitized IBAN, and country code
43
- * @throws Error if value is missing, has an invalid format, or is out of length bounds
44
- */
45
- export function iban(value: string): { valid: boolean; value: string; country?: string } {
46
- checkValue(value);
47
- const cleaned = value.replace(/\s/g, '').toUpperCase();
48
- if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(cleaned)) throw new Error('Invalid IBAN format');
49
- if (cleaned.length < 15 || cleaned.length > 34) throw new Error('IBAN length must be between 15 and 34 characters');
50
-
51
- const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
52
- const numeric = rearranged
53
- .split('')
54
- .map((c) => (c >= 'A' && c <= 'Z' ? (c.charCodeAt(0) - 55).toString() : c))
55
- .join('');
56
-
57
- let remainder = 0;
58
- for (const char of numeric) {
59
- remainder = (remainder * 10 + parseInt(char, 10)) % 97;
60
- }
61
-
62
- return { valid: remainder === 1, value: cleaned, country: cleaned.slice(0, 2) };
63
- }
64
-
65
- /**
66
- * Validates an email address against a basic format check.
67
- *
68
- * @param value - The email address string to validate
69
- * @returns Object containing the validity result and the original value
70
- * @throws Error if value is missing, not a string, or too long
71
- */
72
- export function email(value: string): { valid: boolean; value: string } {
73
- checkValue(value);
74
- const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
75
- return { valid: regex.test(value), value };
76
- }
package/src/modules/v4.ts DELETED
@@ -1,29 +0,0 @@
1
- export { default as address } from './v4/address.js';
2
- export { default as agent } from './v4/agent.js';
3
- export * as algorithms from './v4/algorithms.js';
4
- export { default as captcha } from './v4/captcha.js';
5
- export { default as chat } from './v4/chat.js';
6
- export { default as color } from './v4/color.js';
7
- export { default as convert } from './v4/convert.js';
8
- export { default as cron } from './v4/cron.js';
9
- export { default as dice } from './v4/dice.js';
10
- export { default as domain } from './v4/domain.js';
11
- export * as encode from './v4/encode.js';
12
- export { default as geo } from './v4/geo.js';
13
- export { default as hash } from './v4/hash.js';
14
- export { default as hyperplanning } from './v4/hyperplanning.js';
15
- export { default as ip } from './v4/ip.js';
16
- export { default as levenshtein } from './v4/levenshtein.js';
17
- export { default as palette } from './v4/palette.js';
18
- export { default as password } from './v4/password.js';
19
- export { default as personal } from './v4/personal.js';
20
- export { default as placeholder } from './v4/placeholder.js';
21
- export { default as qrcode } from './v4/qrcode.js';
22
- export { default as regex } from './v4/regex.js';
23
- export { default as statistics } from './v4/statistics.js';
24
- export * as text from './v4/text.js';
25
- export { default as tic_tac_toe } from './v4/tic_tac_toe.js';
26
- export { default as time } from './v4/time.js';
27
- export { default as token } from './v4/token.js';
28
- export { default as username } from './v4/username.js';
29
- export * as validate from './v4/validate.js';
@@ -1,72 +0,0 @@
1
- import { type Request, type Response, Router } from 'express';
2
-
3
- import { chatStorage, ticTacToeStorage } from '../storage/index.js';
4
- import { error } from '../utils/response.js';
5
-
6
- const router = Router();
7
-
8
- // Clear a private chat
9
- router.delete('/:version/chat/:token', (req: Request, res: Response) => {
10
- if (req.version !== 'v4') {
11
- error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/chat`);
12
- return;
13
- }
14
-
15
- const token = req.params.token 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.chat('clear', {
29
- username,
30
- token,
31
- session,
32
- storage: chatStorage,
33
- });
34
- res.jsonResponse(result);
35
- } catch (err) {
36
- error(res, 400, (err as Error).message);
37
- }
38
- });
39
-
40
- // Forfeit a tic-tac-toe game
41
- router.delete('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
42
- if (req.version !== 'v4') {
43
- error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/tic-tac-toe`);
44
- return;
45
- }
46
-
47
- const game = req.params.game 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.tic_tac_toe('forfeit', {
61
- username,
62
- session,
63
- game,
64
- storage: ticTacToeStorage,
65
- });
66
- res.jsonResponse(result);
67
- } catch (err) {
68
- error(res, 400, (err as Error).message);
69
- }
70
- });
71
-
72
- export default router;