@20syldev/api 4.4.0 → 4.6.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 +9 -4
  2. package/dist/config/env.js +5 -1
  3. package/dist/config/env.js.map +1 -1
  4. package/dist/config/plans.js +44 -0
  5. package/dist/config/plans.js.map +1 -0
  6. package/dist/constants.js +1 -0
  7. package/dist/constants.js.map +1 -1
  8. package/dist/middleware/cors.js +7 -2
  9. package/dist/middleware/cors.js.map +1 -1
  10. package/dist/middleware/error.js +3 -1
  11. package/dist/middleware/error.js.map +1 -1
  12. package/dist/middleware/ratelimit.js +39 -21
  13. package/dist/middleware/ratelimit.js.map +1 -1
  14. package/dist/modules/v3/chat.js +1 -1
  15. package/dist/modules/v3/chat.js.map +1 -1
  16. package/dist/modules/v3/domain.js +1 -1
  17. package/dist/modules/v3/domain.js.map +1 -1
  18. package/dist/modules/v3/hyperplanning.js +1 -1
  19. package/dist/modules/v3/hyperplanning.js.map +1 -1
  20. package/dist/modules/v3/personal.js +1 -1
  21. package/dist/modules/v3/personal.js.map +1 -1
  22. package/dist/modules/v3/username.js +1 -1
  23. package/dist/modules/v3/username.js.map +1 -1
  24. package/dist/modules/v4/convert.js.map +1 -1
  25. package/dist/modules/v4/hyperplanning.js +31 -1
  26. package/dist/modules/v4/hyperplanning.js.map +1 -1
  27. package/dist/routes/get.js +9 -8
  28. package/dist/routes/get.js.map +1 -1
  29. package/dist/routes/post.js +2 -1
  30. package/dist/routes/post.js.map +1 -1
  31. package/dist/utils/colors.js +59 -20
  32. package/dist/utils/colors.js.map +1 -1
  33. package/dist/utils/helpers.js +67 -16
  34. package/dist/utils/helpers.js.map +1 -1
  35. package/dist/utils/response.js +18 -2
  36. package/dist/utils/response.js.map +1 -1
  37. package/eslint.config.js +4 -0
  38. package/package.json +3 -2
  39. package/src/app.ts +7 -6
  40. package/src/config/env.ts +7 -1
  41. package/src/config/plans.ts +61 -0
  42. package/src/config/versions.ts +7 -4
  43. package/src/constants.ts +1 -0
  44. package/src/middleware/cors.ts +8 -2
  45. package/src/middleware/error.ts +12 -3
  46. package/src/middleware/json.ts +1 -1
  47. package/src/middleware/logger.ts +2 -1
  48. package/src/middleware/ratelimit.ts +44 -21
  49. package/src/middleware/version.ts +2 -1
  50. package/src/modules/v3.js +1 -1
  51. package/src/modules/v4/agent.ts +104 -0
  52. package/src/modules/v4/algorithms.ts +79 -0
  53. package/src/modules/v4/captcha.ts +8 -0
  54. package/src/modules/v4/chat.ts +10 -2
  55. package/src/modules/v4/color.ts +8 -1
  56. package/src/modules/v4/convert.ts +9 -0
  57. package/src/modules/v4/dice.ts +7 -0
  58. package/src/modules/v4/domain.ts +6 -1
  59. package/src/modules/v4/encode.ts +71 -0
  60. package/src/modules/v4/geo.ts +10 -0
  61. package/src/modules/v4/hash.ts +10 -1
  62. package/src/modules/v4/hyperplanning.ts +37 -2
  63. package/src/modules/v4/ip.ts +134 -0
  64. package/src/modules/v4/levenshtein.ts +8 -0
  65. package/src/modules/v4/palette.ts +9 -1
  66. package/src/modules/v4/personal.ts +5 -0
  67. package/src/modules/v4/placeholder.ts +8 -0
  68. package/src/modules/v4/qrcode.ts +9 -1
  69. package/src/modules/v4/statistics.ts +7 -0
  70. package/src/modules/v4/text.ts +30 -0
  71. package/src/modules/v4/tic_tac_toe.ts +11 -2
  72. package/src/modules/v4/time.ts +11 -0
  73. package/src/modules/v4/token.ts +10 -1
  74. package/src/modules/v4/username.ts +5 -0
  75. package/src/modules/v4/validate.ts +21 -0
  76. package/src/modules/v4.ts +3 -1
  77. package/src/routes/delete.ts +2 -1
  78. package/src/routes/get.ts +61 -11
  79. package/src/routes/index.ts +4 -3
  80. package/src/routes/patch.ts +2 -1
  81. package/src/routes/post.ts +5 -4
  82. package/src/storage/index.ts +1 -1
  83. package/src/utils/response.ts +11 -2
  84. package/tests/integration/api.test.ts +125 -2
  85. package/tests/unit/agent.test.ts +113 -0
  86. package/tests/unit/algorithms.test.ts +2 -1
  87. package/tests/unit/captcha.test.ts +2 -1
  88. package/tests/unit/chat.test.ts +2 -1
  89. package/tests/unit/color.test.ts +2 -1
  90. package/tests/unit/convert.test.ts +2 -1
  91. package/tests/unit/dice.test.ts +2 -1
  92. package/tests/unit/domain.test.ts +2 -1
  93. package/tests/unit/encode.test.ts +8 -7
  94. package/tests/unit/geo.test.ts +2 -1
  95. package/tests/unit/hash.test.ts +2 -1
  96. package/tests/unit/hyperplanning.test.ts +21 -1
  97. package/tests/unit/ip.test.ts +140 -0
  98. package/tests/unit/levenshtein.test.ts +2 -1
  99. package/tests/unit/palette.test.ts +2 -1
  100. package/tests/unit/personal.test.ts +2 -1
  101. package/tests/unit/placeholder.test.ts +2 -1
  102. package/tests/unit/qrcode.test.ts +2 -1
  103. package/tests/unit/statistics.test.ts +2 -1
  104. package/tests/unit/text.test.ts +3 -2
  105. package/tests/unit/tic_tac_toe.test.ts +2 -1
  106. package/tests/unit/time.test.ts +2 -1
  107. package/tests/unit/token.test.ts +2 -1
  108. package/tests/unit/username.test.ts +2 -1
  109. package/tests/unit/validate.test.ts +3 -2
@@ -23,6 +23,16 @@ function parseCoord(value: string, name: string, min: number, max: number): numb
23
23
  return n;
24
24
  }
25
25
 
26
+ /**
27
+ * Calculates the great-circle distance and bearing between two geographic coordinates.
28
+ *
29
+ * @param lat1 - Latitude of the first point (-90 to 90)
30
+ * @param lon1 - Longitude of the first point (-180 to 180)
31
+ * @param lat2 - Latitude of the second point (-90 to 90)
32
+ * @param lon2 - Longitude of the second point (-180 to 180)
33
+ * @returns Distance in km/miles/nautical miles, compass bearing, and both coordinates
34
+ * @throws Error if any coordinate is out of range or not a number
35
+ */
26
36
  export default function geo(lat1: string, lon1: string, lat2: string, lon2: string): GeoResult {
27
37
  const a = parseCoord(lat1, 'lat1', -90, 90);
28
38
  const b = parseCoord(lon1, 'lon1', -180, 180);
@@ -1,4 +1,4 @@
1
- import { getHashes, createHash } from 'crypto';
1
+ import { createHash, getHashes } from 'crypto';
2
2
 
3
3
  export interface HashResult {
4
4
  method: string;
@@ -8,6 +8,15 @@ export interface HashResult {
8
8
 
9
9
  const ENCODINGS = new Set(['hex', 'base64']);
10
10
 
11
+ /**
12
+ * Hashes a text string using the specified algorithm and encoding.
13
+ *
14
+ * @param text - The text to hash
15
+ * @param method - Hashing algorithm (e.g. "sha256", "md5")
16
+ * @param encoding - Output encoding: "hex" (default) or "base64"
17
+ * @returns Object containing the method, hash result, and encoding used
18
+ * @throws Error if text is missing, the method is unsupported, or the encoding is invalid
19
+ */
11
20
  export default function hash(text: string, method: string, encoding: string = 'hex'): HashResult {
12
21
  if (!text) throw new Error('Text is required');
13
22
 
@@ -1,6 +1,7 @@
1
- import { formatDate } from '../../utils/helpers.js';
2
1
  import ical from 'ical.js';
3
2
 
3
+ import { formatDate } from '../../utils/helpers.js';
4
+
4
5
  interface CalendarEvent {
5
6
  summary: string[] | string;
6
7
  subject?: string;
@@ -11,8 +12,42 @@ interface CalendarEvent {
11
12
  end: string;
12
13
  }
13
14
 
15
+ function blocked(hostname: string): boolean {
16
+ const list = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]', 'metadata.google.internal'];
17
+ if (list.includes(hostname)) return true;
18
+
19
+ const parts = hostname.split('.').map(Number);
20
+ if (parts.length === 4 && parts.every((n) => !isNaN(n))) {
21
+ if (parts[0] === 10) return true;
22
+ if (parts[0] === 172 && parts[1]! >= 16 && parts[1]! <= 31) return true;
23
+ if (parts[0] === 192 && parts[1] === 168) return true;
24
+ if (parts[0] === 169 && parts[1] === 254) return true;
25
+ if (parts[0] === 0) return true;
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Fetches and parses an ICS calendar from a Hyperplanning URL, filtering out past events.
33
+ *
34
+ * @param url - HTTPS URL to the ICS calendar
35
+ * @param detail - Level of detail: "full" (subject/teacher/classes), "list" (summary/times), or default (summary only)
36
+ * @returns Array of calendar events sorted by start time, excluding past events
37
+ * @throws Error if the URL is invalid, uses HTTP, points to a private host, or does not return a valid ICS file
38
+ */
14
39
  export default async function hyperplanning(url: string, detail?: string): Promise<CalendarEvent[]> {
15
- const response = await fetch(url);
40
+ let parsed: URL;
41
+ try {
42
+ parsed = new URL(url);
43
+ } catch {
44
+ throw new Error('Invalid URL.');
45
+ }
46
+
47
+ if (parsed.protocol !== 'https:') throw new Error('Only HTTPS URLs are allowed.');
48
+ if (blocked(parsed.hostname)) throw new Error('Access to private/internal hosts is not allowed.');
49
+
50
+ const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
16
51
 
17
52
  if (!response.ok || !(response.headers.get('content-type') || '').includes('text/calendar')) {
18
53
  throw new Error('Invalid ICS file format.');
@@ -0,0 +1,134 @@
1
+ import { MAX_STRING_LENGTH } from '../../constants.js';
2
+
3
+ export interface IpResult {
4
+ ip: string;
5
+ version: 'IPv4' | 'IPv6';
6
+ type: 'public' | 'private' | 'loopback' | 'link-local' | 'multicast' | 'broadcast';
7
+ class?: 'A' | 'B' | 'C' | 'D' | 'E';
8
+ range?: string;
9
+ binary: string;
10
+ decimal?: number;
11
+ reverse: string;
12
+ }
13
+
14
+ /**
15
+ * Parses and analyzes an IPv4 address.
16
+ *
17
+ * @param raw - The IPv4 address string (e.g. "192.168.1.1")
18
+ * @returns Detailed information about the address
19
+ * @throws Error if the address is malformed
20
+ */
21
+ function parseIPv4(raw: string): IpResult {
22
+ const parts = raw.split('.');
23
+ if (parts.length !== 4) throw new Error('Invalid IPv4 address');
24
+ const octets = parts.map(Number);
25
+ if (octets.some((o) => isNaN(o) || !Number.isInteger(o) || o < 0 || o > 255))
26
+ throw new Error('Invalid IPv4 address');
27
+
28
+ const [a, b, c, d] = octets as [number, number, number, number];
29
+
30
+ let cls: 'A' | 'B' | 'C' | 'D' | 'E';
31
+ if (a < 128) cls = 'A';
32
+ else if (a < 192) cls = 'B';
33
+ else if (a < 224) cls = 'C';
34
+ else if (a < 240) cls = 'D';
35
+ else cls = 'E';
36
+
37
+ let type: IpResult['type'] = 'public';
38
+ let range: string | undefined;
39
+
40
+ if (a === 127) {
41
+ type = 'loopback';
42
+ range = '127.0.0.0/8';
43
+ } else if (a === 10) {
44
+ type = 'private';
45
+ range = '10.0.0.0/8';
46
+ } else if (a === 172 && b >= 16 && b <= 31) {
47
+ type = 'private';
48
+ range = '172.16.0.0/12';
49
+ } else if (a === 192 && b === 168) {
50
+ type = 'private';
51
+ range = '192.168.0.0/16';
52
+ } else if (a === 169 && b === 254) {
53
+ type = 'link-local';
54
+ range = '169.254.0.0/16';
55
+ } else if (a >= 224 && a <= 239) {
56
+ type = 'multicast';
57
+ range = '224.0.0.0/4';
58
+ } else if (raw === '255.255.255.255') {
59
+ type = 'broadcast';
60
+ }
61
+
62
+ const binary = octets.map((o) => o.toString(2).padStart(8, '0')).join('.');
63
+ const decimal = ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
64
+ const reverse = [...octets].reverse().join('.') + '.in-addr.arpa';
65
+
66
+ const result: IpResult = { ip: raw, version: 'IPv4', type, class: cls, binary, decimal, reverse };
67
+ if (range) result.range = range;
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Parses and analyzes an IPv6 address, expanding :: shorthand.
73
+ *
74
+ * @param raw - The IPv6 address string (e.g. "::1" or "fe80::1")
75
+ * @returns Detailed information about the address
76
+ * @throws Error if the address is malformed
77
+ */
78
+ function parseIPv6(raw: string): IpResult {
79
+ let expanded = raw;
80
+ if (raw.includes('::')) {
81
+ const halves = raw.split('::');
82
+ const left = halves[0] ? halves[0].split(':') : [];
83
+ const right = halves[1] ? halves[1].split(':') : [];
84
+ const fill = Array(8 - left.length - right.length).fill('0');
85
+ expanded = [...left, ...fill, ...right].join(':');
86
+ }
87
+
88
+ const groups = expanded.split(':');
89
+ if (groups.length !== 8) throw new Error('Invalid IPv6 address');
90
+ const values = groups.map((g) => parseInt(g || '0', 16));
91
+ if (values.some((v) => isNaN(v) || v < 0 || v > 0xffff)) throw new Error('Invalid IPv6 address');
92
+
93
+ let type: IpResult['type'] = 'public';
94
+ let range: string | undefined;
95
+
96
+ if (raw === '::1' || values.every((v, i) => (i < 7 ? v === 0 : v === 1))) {
97
+ type = 'loopback';
98
+ range = '::1/128';
99
+ } else if ((values[0]! & 0xfe00) === 0xfc00) {
100
+ type = 'private';
101
+ range = 'fc00::/7';
102
+ } else if ((values[0]! & 0xffc0) === 0xfe80) {
103
+ type = 'link-local';
104
+ range = 'fe80::/10';
105
+ } else if ((values[0]! & 0xff00) === 0xff00) {
106
+ type = 'multicast';
107
+ range = 'ff00::/8';
108
+ }
109
+
110
+ const binary = values.map((v) => v.toString(2).padStart(16, '0')).join(':');
111
+ const hex = values.map((v) => v.toString(16).padStart(4, '0')).join('');
112
+ const reverse = hex.split('').reverse().join('.') + '.ip6.arpa';
113
+
114
+ const result: IpResult = { ip: expanded, version: 'IPv6', type, binary, reverse };
115
+ if (range) result.range = range;
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * Analyzes an IPv4 or IPv6 address and returns its type, class, binary
121
+ * representation, decimal value, and reverse DNS notation.
122
+ *
123
+ * @param address - The IP address to analyze
124
+ * @returns Detailed breakdown of the address
125
+ * @throws Error if the address is missing or invalid
126
+ */
127
+ export default function ip(address: string): IpResult {
128
+ if (!address || typeof address !== 'string') throw new Error('An IP address is required');
129
+ if (address.length > MAX_STRING_LENGTH) throw new Error('Address is too long');
130
+
131
+ if (address.includes(':')) return parseIPv6(address);
132
+ if (address.includes('.')) return parseIPv4(address);
133
+ throw new Error('Invalid IP address format');
134
+ }
@@ -1,5 +1,13 @@
1
1
  import { MAX_LEVENSHTEIN_LENGTH } from '../../constants.js';
2
2
 
3
+ /**
4
+ * Computes the Levenshtein edit distance between two strings.
5
+ *
6
+ * @param str1 - First string
7
+ * @param str2 - Second string
8
+ * @returns Object containing both strings and the minimum number of single-character edits to transform one into the other
9
+ * @throws Error if either string is missing, not a string, or exceeds the maximum length
10
+ */
3
11
  export default function levenshtein(str1: string, str2: string): { str1: string; str2: string; distance: number } {
4
12
  if (!str1 || typeof str1 !== 'string') {
5
13
  throw new Error('Please provide a valid first string');
@@ -1,4 +1,4 @@
1
- import { hexToRgb, rgbToHsl, hslToRgb, rgbToHex } from '../../utils/colors.js';
1
+ import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../../utils/colors.js';
2
2
 
3
3
  export interface PaletteColor {
4
4
  hex: string;
@@ -30,6 +30,14 @@ function fromHueShifts(base: [number, number, number], shifts: number[]): Palett
30
30
  });
31
31
  }
32
32
 
33
+ /**
34
+ * Generates a color palette from a base hex color using a specified harmony type.
35
+ *
36
+ * @param color - Base hex color (e.g. "#ff5733")
37
+ * @param type - Palette type: "complementary", "triadic", "analogous", "tetradic", or "split-complementary"
38
+ * @returns Object containing the base color and the derived palette colors in hex, RGB, and HSL
39
+ * @throws Error if color or type is missing, or the type is not one of the accepted values
40
+ */
33
41
  export default function palette(color: string, type: string): PaletteResult {
34
42
  if (!color) throw new Error('A base color is required');
35
43
  if (!type) throw new Error('A palette type is required');
@@ -13,6 +13,11 @@ interface CountryInfo {
13
13
  lang: string;
14
14
  }
15
15
 
16
+ /**
17
+ * Generates a fictional personal profile with realistic demographic and contact data.
18
+ *
19
+ * @returns Object containing name, email, phone, address, job, hobbies, and other personal attributes
20
+ */
16
21
  export default function personal(): Record<string, unknown> {
17
22
  const people: Person[] = [
18
23
  { name: 'John Doe', social: 'john_doe', email: 'john@example.com', country: 'US' },
@@ -157,6 +157,14 @@ function parseAnimate(value: string | undefined): AnimateMode {
157
157
  throw new Error('animate must be one of: shimmer, pulse, none');
158
158
  }
159
159
 
160
+ /**
161
+ * Generates an SVG placeholder image or skeleton loader with configurable dimensions and style.
162
+ *
163
+ * @param type - Placeholder type: "image" or "skeleton"
164
+ * @param query - Query parameters including width, height, bg, color, text, rows, avatar, animate, speed, and radius
165
+ * @returns Object containing the SVG body, content type, and placeholder type
166
+ * @throws Error if any parameter is invalid or out of range
167
+ */
160
168
  export default function placeholder(type: string, query: Record<string, string | undefined>): PlaceholderResult {
161
169
  const speed = query.speed ? parseFloat(query.speed) : 1.5;
162
170
  if (isNaN(speed) || speed < 0.1 || speed > 10) throw new Error('speed must be between 0.1 and 10');
@@ -1,5 +1,6 @@
1
- import { toBuffer, toDataURL, toCanvas as qrToCanvas } from 'qrcode';
2
1
  import { createCanvas, loadImage } from 'canvas';
2
+ import { toBuffer, toCanvas as qrToCanvas, toDataURL } from 'qrcode';
3
+
3
4
  import { normalizeColor } from '../../utils/colors.js';
4
5
 
5
6
  export interface QRCodeOptions {
@@ -48,6 +49,13 @@ async function fetchIcon(url: string): Promise<Buffer> {
48
49
  return buffer;
49
50
  }
50
51
 
52
+ /**
53
+ * Generates a QR code image for a given URL, with optional icon overlay and color customization.
54
+ *
55
+ * @param options - QR code generation options including URL, size, margin, correction level, colors, and optional icon
56
+ * @returns Object containing the QR code as a PNG buffer or Base64 string, and the content type
57
+ * @throws Error if the URL is missing, any option is invalid, or the icon URL cannot be fetched
58
+ */
51
59
  export default async function qrcode(options: QRCodeOptions): Promise<QRCodeResult> {
52
60
  const { url } = options;
53
61
  if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
@@ -13,6 +13,13 @@ export interface StatisticsResult {
13
13
  stddev: number;
14
14
  }
15
15
 
16
+ /**
17
+ * Computes descriptive statistics for a comma-separated list of numbers.
18
+ *
19
+ * @param values - Comma-separated numeric string (e.g. "1,2,3,4,5")
20
+ * @returns Object containing count, sum, min, max, range, mean, median, mode, variance, and standard deviation
21
+ * @throws Error if values is missing, contains non-numeric entries, or exceeds the maximum count
22
+ */
16
23
  export default function statistics(values: string): StatisticsResult {
17
24
  if (!values) throw new Error('A list of values is required');
18
25
  if (typeof values !== 'string') throw new Error('Values must be a comma-separated string');
@@ -142,6 +142,13 @@ export interface TextStats {
142
142
  mostFrequentChar: string;
143
143
  }
144
144
 
145
+ /**
146
+ * Analyzes a text string and returns character, word, sentence, and reading time statistics.
147
+ *
148
+ * @param value - The text to analyze
149
+ * @returns Object containing character counts, word/sentence/paragraph counts, reading time, and most frequent character
150
+ * @throws Error if value is missing, not a string, or too long
151
+ */
145
152
  export function stats(value: string): TextStats {
146
153
  checkText(value);
147
154
 
@@ -168,6 +175,13 @@ export function stats(value: string): TextStats {
168
175
  return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, mostFrequentChar };
169
176
  }
170
177
 
178
+ /**
179
+ * Converts a string to a URL-friendly slug.
180
+ *
181
+ * @param value - The string to slugify
182
+ * @returns Lowercase hyphenated slug with diacritics and special characters removed
183
+ * @throws Error if value is missing, not a string, or too long
184
+ */
171
185
  export function slug(value: string): string {
172
186
  checkText(value);
173
187
  return value
@@ -179,6 +193,14 @@ export function slug(value: string): string {
179
193
  .replace(/[\s-]+/g, '-');
180
194
  }
181
195
 
196
+ /**
197
+ * Generates Lorem Ipsum placeholder text.
198
+ *
199
+ * @param type - Content type: "words", "sentences", or "paragraphs"
200
+ * @param count - Number of words, sentences, or paragraphs to generate
201
+ * @returns Generated Lorem Ipsum text
202
+ * @throws Error if count is out of range or type is not one of the accepted values
203
+ */
182
204
  export function lorem(type: string, count: string): string {
183
205
  const n = Number(count) || 5;
184
206
  if (n < 1 || n > 500) throw new Error('Count must be between 1 and 500');
@@ -283,6 +305,14 @@ function numberToEnglish(n: number): string {
283
305
  throw new Error('Number must be less than 1 billion');
284
306
  }
285
307
 
308
+ /**
309
+ * Converts an integer to its written-out word form in French or English.
310
+ *
311
+ * @param value - The integer to convert (must be less than 1 billion)
312
+ * @param lang - Language: "fr" for French or "en" for English
313
+ * @returns The number written out in words
314
+ * @throws Error if value is not an integer, exceeds the maximum, or lang is not supported
315
+ */
286
316
  export function number(value: string, lang: string): string {
287
317
  const n = Number(value);
288
318
  if (isNaN(n)) throw new Error('Value must be a number');
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from 'crypto';
2
- import { SESSION_TTL, GAME_CLEANUP_TTL, RATE_LIMIT_WINDOW, RATE_LIMIT_MAX } from '../../constants.js';
3
- import type { TicTacToeStorage, TicTacToeMove, TicTacToeGame } from '../../types/storage.js';
2
+
3
+ import { GAME_CLEANUP_TTL, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, SESSION_TTL } from '../../constants.js';
4
+ import type { TicTacToeGame, TicTacToeMove, TicTacToeStorage } from '../../types/storage.js';
4
5
 
5
6
  interface TicTacToeParams {
6
7
  username?: string;
@@ -17,6 +18,14 @@ interface GameResult {
17
18
  tie?: boolean;
18
19
  }
19
20
 
21
+ /**
22
+ * Handles Tic-Tac-Toe game actions including playing a move, fetching game state, listing games, and forfeiting.
23
+ *
24
+ * @param action - The action to perform: "play", "fetch", "list", or "forfeit"
25
+ * @param params - Game parameters including username, move, session, game ID, and shared storage
26
+ * @returns Game state or action result depending on the action
27
+ * @throws Error if a required parameter is missing, the action is invalid, or a rate limit is exceeded
28
+ */
20
29
  export default function tic_tac_toe(action: string, params: TicTacToeParams): Record<string, unknown> {
21
30
  const storage = params.storage;
22
31
 
@@ -24,6 +24,17 @@ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo',
24
24
  type TimeFormat = (typeof validFormats)[number];
25
25
  type Timezone = (typeof validTimezones)[number];
26
26
 
27
+ /**
28
+ * Returns the current or a random date/time in various formats and timezones.
29
+ *
30
+ * @param type - "live" for the current time or "random" for a random date within a range
31
+ * @param start - Optional start date for random mode (YYYY-MM-DD)
32
+ * @param end - Optional end date for random mode (YYYY-MM-DD)
33
+ * @param format - Optional specific format to return (e.g. "iso", "timestamp", "year")
34
+ * @param timezone - Optional timezone (e.g. "UTC", "Europe/Paris")
35
+ * @returns Object containing all time formats, or a single format if specified
36
+ * @throws Error if type, format, or timezone is invalid
37
+ */
27
38
  export default function time(
28
39
  type: string = 'live',
29
40
  start?: string,
@@ -1,7 +1,16 @@
1
1
  import { randomBytes } from 'crypto';
2
2
  import { v4 } from 'uuid';
3
- import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../../constants.js';
4
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
+ */
5
14
  export default function token(len: number, type: string = 'alphanum'): string {
6
15
  if (isNaN(len) || len < MIN_TOKEN_LENGTH) {
7
16
  throw new Error('Length must be a number greater than or equal to 12');
@@ -1,5 +1,10 @@
1
1
  import { random, randomNumber } from '../../utils/helpers.js';
2
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
+ */
3
8
  export default function username(): Record<string, unknown> {
4
9
  const adj = [
5
10
  'Happy',
@@ -7,6 +7,13 @@ function checkValue(value: string): void {
7
7
  throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
8
8
  }
9
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
+ */
10
17
  export function luhn(value: string): { valid: boolean; value: string } {
11
18
  checkValue(value);
12
19
  const digits = value.replace(/\s|-/g, '');
@@ -28,6 +35,13 @@ export function luhn(value: string): { valid: boolean; value: string } {
28
35
  return { valid: sum % 10 === 0, value: digits };
29
36
  }
30
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
+ */
31
45
  export function iban(value: string): { valid: boolean; value: string; country?: string } {
32
46
  checkValue(value);
33
47
  const cleaned = value.replace(/\s/g, '').toUpperCase();
@@ -48,6 +62,13 @@ export function iban(value: string): { valid: boolean; value: string; country?:
48
62
  return { valid: remainder === 1, value: cleaned, country: cleaned.slice(0, 2) };
49
63
  }
50
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
+ */
51
72
  export function email(value: string): { valid: boolean; value: string } {
52
73
  checkValue(value);
53
74
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
package/src/modules/v4.ts CHANGED
@@ -1,6 +1,7 @@
1
+ export { default as agent } from './v4/agent.js';
1
2
  export * as algorithms from './v4/algorithms.js';
2
- export { default as chat } from './v4/chat.js';
3
3
  export { default as captcha } from './v4/captcha.js';
4
+ export { default as chat } from './v4/chat.js';
4
5
  export { default as color } from './v4/color.js';
5
6
  export { default as convert } from './v4/convert.js';
6
7
  export { default as dice } from './v4/dice.js';
@@ -9,6 +10,7 @@ export * as encode from './v4/encode.js';
9
10
  export { default as geo } from './v4/geo.js';
10
11
  export { default as hash } from './v4/hash.js';
11
12
  export { default as hyperplanning } from './v4/hyperplanning.js';
13
+ export { default as ip } from './v4/ip.js';
12
14
  export { default as levenshtein } from './v4/levenshtein.js';
13
15
  export { default as palette } from './v4/palette.js';
14
16
  export { default as personal } from './v4/personal.js';
@@ -1,4 +1,5 @@
1
- import { Router, type Request, type Response } from 'express';
1
+ import { type Request, type Response, Router } from 'express';
2
+
2
3
  import { chatStorage, ticTacToeStorage } from '../storage/index.js';
3
4
  import { error } from '../utils/response.js';
4
5
 
package/src/routes/get.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { Router, type Request, type Response } from 'express';
2
- import { versions } from '../config/versions.js';
1
+ import { type Request, type Response, Router } from 'express';
2
+
3
3
  import { env } from '../config/env.js';
4
- import { ipLimits } from '../storage/index.js';
5
- import { chatStorage } from '../storage/index.js';
4
+ import { versions } from '../config/versions.js';
6
5
  import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
7
- import { error } from '../utils/response.js';
8
- import { since } from '../utils/helpers.js';
9
- import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
6
+ import type { UserAgentResult } from '../modules/v4/agent.js';
10
7
  import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
11
8
  import type { ColorResult } from '../modules/v4/color.js';
9
+ import type { IpResult } from '../modules/v4/ip.js';
10
+ import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
11
+ import { chatStorage, ipLimits } from '../storage/index.js';
12
+ import { since } from '../utils/helpers.js';
13
+ import { error } from '../utils/response.js';
12
14
 
13
15
  const router = Router();
14
16
 
@@ -61,7 +63,7 @@ router.get('/:version/algorithms', (req: Request, res: Response) => {
61
63
  const { version } = req.params;
62
64
 
63
65
  const algorithms = req.module.algorithms as Record<string, (v: string, v2?: string) => unknown>;
64
- if (!algorithms || !algorithms[method as string]) {
66
+ if (!algorithms || !method || !Object.hasOwn(algorithms, method as string)) {
65
67
  error(res, 400, 'Please provide a valid algorithm (?method={algorithm})', `${version}/algorithms`);
66
68
  return;
67
69
  }
@@ -174,6 +176,54 @@ router.get('/:version/convert', (req: Request, res: Response) => {
174
176
  }
175
177
  });
176
178
 
179
+ // Echo request headers
180
+ router.get('/:version/headers', (req: Request, res: Response) => {
181
+ const redacted = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
182
+ let headers: Record<string, unknown> = {};
183
+
184
+ for (const [k, v] of Object.entries(req.headers)) {
185
+ headers[k] = redacted.has(k) ? '[redacted]' : v;
186
+ }
187
+
188
+ const filter = req.query.filter as string | undefined;
189
+ if (filter) {
190
+ const keys = new Set(filter.split(',').map((k) => k.trim().toLowerCase()));
191
+ headers = Object.fromEntries(Object.entries(headers).filter(([k]) => keys.has(k)));
192
+ }
193
+
194
+ res.jsonResponse({
195
+ count: Object.keys(headers).length,
196
+ headers,
197
+ ip: req.ip,
198
+ method: req.method,
199
+ url: req.originalUrl,
200
+ });
201
+ });
202
+
203
+ // Analyze an IP address
204
+ router.get('/:version/ip', (req: Request, res: Response) => {
205
+ const address = (req.query.address as string | undefined) ?? req.ip ?? '';
206
+ try {
207
+ const ipFn = (req.module as Record<string, unknown>).ip as (a: string) => IpResult;
208
+ const result = ipFn(address);
209
+ res.jsonResponse(result);
210
+ } catch (err) {
211
+ error(res, 400, (err as Error).message, `${req.version}/ip`);
212
+ }
213
+ });
214
+
215
+ // Parse a User-Agent string
216
+ router.get('/:version/agent', (req: Request, res: Response) => {
217
+ const ua = (req.query.ua as string | undefined) ?? (req.headers['user-agent'] as string) ?? '';
218
+ try {
219
+ const agentFn = (req.module as Record<string, unknown>).agent as (ua: string) => UserAgentResult;
220
+ const result = agentFn(ua);
221
+ res.jsonResponse(result);
222
+ } catch (err) {
223
+ error(res, 400, (err as Error).message, `${req.version}/agent`);
224
+ }
225
+ });
226
+
177
227
  // Generate domain informations
178
228
  router.get('/:version/domain', (req: Request, res: Response) => {
179
229
  try {
@@ -217,7 +267,7 @@ router.get('/:version/encode', (req: Request, res: Response) => {
217
267
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/encode`);
218
268
  return;
219
269
  }
220
- if (!method || !encode[method as string]) {
270
+ if (!method || !Object.hasOwn(encode, method as string)) {
221
271
  error(res, 400, 'Please provide a valid method (?method={method})', `${version}/encode`);
222
272
  return;
223
273
  }
@@ -434,7 +484,7 @@ router.get('/:version/text', (req: Request, res: Response) => {
434
484
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/text`);
435
485
  return;
436
486
  }
437
- if (!method || !textMod[method as string]) {
487
+ if (!method || !Object.hasOwn(textMod, method as string)) {
438
488
  error(res, 400, 'Please provide a valid method (?method={slug|stats|lorem|number})', `${version}/text`);
439
489
  return;
440
490
  }
@@ -471,7 +521,7 @@ router.get('/:version/validate', (req: Request, res: Response) => {
471
521
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/validate`);
472
522
  return;
473
523
  }
474
- if (!type || !validate[type as string]) {
524
+ if (!type || !Object.hasOwn(validate, type as string)) {
475
525
  error(res, 400, 'Please provide a valid type (?type={luhn|iban|email})', `${version}/validate`);
476
526
  return;
477
527
  }
@@ -1,8 +1,9 @@
1
- import { Router, type Request, type Response } from 'express';
1
+ import { type Request, type Response, Router } from 'express';
2
+
2
3
  import { versions } from '../config/versions.js';
3
- import { ipLimits } from '../storage/index.js';
4
- import { logger } from '../middleware/logger.js';
5
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';
6
7
 
7
8
  const router = Router();
8
9