@20syldev/api 4.5.0 → 4.7.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 (75) hide show
  1. package/README.md +11 -4
  2. package/eslint.config.js +4 -0
  3. package/package.json +3 -2
  4. package/src/app.ts +7 -6
  5. package/src/config/env.ts +1 -0
  6. package/src/config/versions.ts +10 -2
  7. package/src/middleware/error.ts +2 -1
  8. package/src/middleware/json.ts +1 -1
  9. package/src/middleware/logger.ts +2 -1
  10. package/src/middleware/ratelimit.ts +4 -3
  11. package/src/middleware/version.ts +2 -1
  12. package/src/modules/v3.js +1 -1
  13. package/src/modules/v4/address.ts +413 -0
  14. package/src/modules/v4/agent.ts +104 -0
  15. package/src/modules/v4/algorithms.ts +79 -0
  16. package/src/modules/v4/captcha.ts +8 -0
  17. package/src/modules/v4/chat.ts +10 -2
  18. package/src/modules/v4/color.ts +8 -1
  19. package/src/modules/v4/convert.ts +9 -0
  20. package/src/modules/v4/dice.ts +7 -0
  21. package/src/modules/v4/domain.ts +6 -1
  22. package/src/modules/v4/encode.ts +71 -0
  23. package/src/modules/v4/geo.ts +10 -0
  24. package/src/modules/v4/hash.ts +10 -1
  25. package/src/modules/v4/hyperplanning.ts +10 -1
  26. package/src/modules/v4/ip.ts +134 -0
  27. package/src/modules/v4/levenshtein.ts +8 -0
  28. package/src/modules/v4/palette.ts +9 -1
  29. package/src/modules/v4/password.ts +381 -0
  30. package/src/modules/v4/personal.ts +9 -16
  31. package/src/modules/v4/placeholder.ts +8 -0
  32. package/src/modules/v4/qrcode.ts +9 -1
  33. package/src/modules/v4/statistics.ts +7 -0
  34. package/src/modules/v4/text.ts +30 -0
  35. package/src/modules/v4/tic_tac_toe.ts +11 -2
  36. package/src/modules/v4/time.ts +11 -0
  37. package/src/modules/v4/token.ts +18 -8
  38. package/src/modules/v4/username.ts +5 -0
  39. package/src/modules/v4/validate.ts +21 -0
  40. package/src/modules/v4.ts +5 -1
  41. package/src/routes/delete.ts +2 -1
  42. package/src/routes/get.ts +115 -7
  43. package/src/routes/index.ts +4 -3
  44. package/src/routes/patch.ts +2 -1
  45. package/src/routes/post.ts +5 -4
  46. package/src/storage/index.ts +1 -1
  47. package/src/utils/response.ts +1 -0
  48. package/tests/integration/api.test.ts +142 -2
  49. package/tests/unit/address.test.ts +84 -0
  50. package/tests/unit/agent.test.ts +113 -0
  51. package/tests/unit/algorithms.test.ts +2 -1
  52. package/tests/unit/captcha.test.ts +2 -1
  53. package/tests/unit/chat.test.ts +2 -1
  54. package/tests/unit/color.test.ts +2 -1
  55. package/tests/unit/convert.test.ts +2 -1
  56. package/tests/unit/dice.test.ts +2 -1
  57. package/tests/unit/domain.test.ts +2 -1
  58. package/tests/unit/encode.test.ts +8 -7
  59. package/tests/unit/geo.test.ts +2 -1
  60. package/tests/unit/hash.test.ts +2 -1
  61. package/tests/unit/hyperplanning.test.ts +2 -1
  62. package/tests/unit/ip.test.ts +140 -0
  63. package/tests/unit/levenshtein.test.ts +2 -1
  64. package/tests/unit/palette.test.ts +2 -1
  65. package/tests/unit/password.test.ts +86 -0
  66. package/tests/unit/personal.test.ts +2 -1
  67. package/tests/unit/placeholder.test.ts +2 -1
  68. package/tests/unit/qrcode.test.ts +2 -1
  69. package/tests/unit/statistics.test.ts +2 -1
  70. package/tests/unit/text.test.ts +3 -2
  71. package/tests/unit/tic_tac_toe.test.ts +2 -1
  72. package/tests/unit/time.test.ts +2 -1
  73. package/tests/unit/token.test.ts +2 -1
  74. package/tests/unit/username.test.ts +2 -1
  75. package/tests/unit/validate.test.ts +3 -2
@@ -0,0 +1,104 @@
1
+ import { MAX_STRING_LENGTH } from '../../constants.js';
2
+
3
+ export interface UserAgentResult {
4
+ raw: string;
5
+ browser: { name: string; version: string; major: string };
6
+ os: { name: string; version: string };
7
+ device: { type: 'mobile' | 'tablet' | 'desktop'; vendor: string };
8
+ engine: { name: string; version: string };
9
+ bot: boolean;
10
+ }
11
+
12
+ const WINDOWS_MAP: Record<string, string> = {
13
+ '10.0': 'Windows 10/11',
14
+ '6.3': 'Windows 8.1',
15
+ '6.2': 'Windows 8',
16
+ '6.1': 'Windows 7',
17
+ '6.0': 'Windows Vista',
18
+ '5.1': 'Windows XP',
19
+ };
20
+
21
+ /**
22
+ * Parses a User-Agent string and extracts browser, OS, device, engine
23
+ * and bot information.
24
+ *
25
+ * @param ua - The User-Agent string to parse
26
+ * @returns Structured breakdown of the User-Agent
27
+ * @throws Error if the User-Agent string is missing or too long
28
+ */
29
+ export default function agent(ua: string): UserAgentResult {
30
+ if (!ua || typeof ua !== 'string') throw new Error('A User-Agent string is required');
31
+ if (ua.length > MAX_STRING_LENGTH) throw new Error(`User-Agent must be less than ${MAX_STRING_LENGTH} characters`);
32
+
33
+ const unknown = { name: 'unknown', version: 'unknown', major: 'unknown' };
34
+
35
+ const bot = /bot|crawl|spider|lighthouse|headless|prerender|slurp|bingpreview/i.test(ua);
36
+
37
+ // Browser — order matters: Edge/Opera before Chrome
38
+ let browser = { ...unknown };
39
+ const browserRules: [RegExp, string][] = [
40
+ [/Edg\/(\d+[\d.]*)/i, 'Edge'],
41
+ [/OPR\/(\d+[\d.]*)/i, 'Opera'],
42
+ [/SamsungBrowser\/(\d+[\d.]*)/i, 'Samsung Internet'],
43
+ [/Chrome\/(\d+[\d.]*)/i, 'Chrome'],
44
+ [/Firefox\/(\d+[\d.]*)/i, 'Firefox'],
45
+ [/Version\/([\d.]+).*Safari/i, 'Safari'],
46
+ [/MSIE ([\d.]+)/i, 'Internet Explorer'],
47
+ [/Trident\/.*rv:([\d.]+)/i, 'Internet Explorer'],
48
+ ];
49
+ for (const [re, name] of browserRules) {
50
+ const m = ua.match(re);
51
+ if (m) {
52
+ browser = { name, version: m[1]!, major: m[1]!.split('.')[0]! };
53
+ break;
54
+ }
55
+ }
56
+
57
+ // OS
58
+ let os = { name: 'unknown', version: 'unknown' };
59
+ const osRules: [RegExp, (m: RegExpMatchArray) => typeof os][] = [
60
+ [/iPhone OS ([\d_]+)/i, (m) => ({ name: 'iOS', version: m[1]!.replace(/_/g, '.') })],
61
+ [/iPad.*OS ([\d_]+)/i, (m) => ({ name: 'iPadOS', version: m[1]!.replace(/_/g, '.') })],
62
+ [/Android ([\d.]+)/i, (m) => ({ name: 'Android', version: m[1]! })],
63
+ [/Windows NT ([\d.]+)/i, (m) => ({ name: WINDOWS_MAP[m[1]!] ?? 'Windows', version: m[1]! })],
64
+ [/Mac OS X ([\d_.]+)/i, (m) => ({ name: 'macOS', version: m[1]!.replace(/_/g, '.') })],
65
+ [/Linux/i, () => ({ name: 'Linux', version: 'unknown' })],
66
+ ];
67
+ for (const [re, build] of osRules) {
68
+ const m = ua.match(re);
69
+ if (m) {
70
+ os = build(m);
71
+ break;
72
+ }
73
+ }
74
+
75
+ // Device
76
+ const isMobile = /Mobi|Android|iPhone|iPod/i.test(ua) && !/iPad/i.test(ua);
77
+ const isTablet = /iPad/i.test(ua) || (/Android/i.test(ua) && !/Mobile/i.test(ua));
78
+ const deviceType: 'mobile' | 'tablet' | 'desktop' = isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop';
79
+
80
+ let vendor = 'unknown';
81
+ if (/iPhone|iPad|Macintosh/i.test(ua)) vendor = 'Apple';
82
+ else if (/Samsung/i.test(ua)) vendor = 'Samsung';
83
+ else if (/Pixel|Nexus/i.test(ua)) vendor = 'Google';
84
+ else if (/Huawei/i.test(ua)) vendor = 'Huawei';
85
+
86
+ // Engine
87
+ let engine = { name: 'unknown', version: 'unknown' };
88
+ const engineRules: [RegExp, string][] = [
89
+ [/AppleWebKit\/([\d.]+)/i, 'WebKit'],
90
+ [/Gecko\/([\d.]+)/i, 'Gecko'],
91
+ [/Trident\/([\d.]+)/i, 'Trident'],
92
+ ];
93
+ for (const [re, name] of engineRules) {
94
+ const m = ua.match(re);
95
+ if (m) {
96
+ // Chrome and Edge use Blink, a WebKit fork
97
+ const engineName = name === 'WebKit' && /Chrome|Edg/i.test(ua) ? 'Blink' : name;
98
+ engine = { name: engineName, version: m[1]! };
99
+ break;
100
+ }
101
+ }
102
+
103
+ return { raw: ua, browser, os, device: { type: deviceType, vendor }, engine, bot };
104
+ }
@@ -1,5 +1,13 @@
1
1
  import { MAX_FACTORIAL, MAX_GCD_VALUE, MAX_PRIME_LIST, MAX_STRING_LENGTH, ROMAN_VALUES } from '../../constants.js';
2
2
 
3
+ /**
4
+ * Checks whether two strings are anagrams of each other.
5
+ *
6
+ * @param value - First string
7
+ * @param value2 - Second string
8
+ * @returns True if the strings are anagrams, false otherwise
9
+ * @throws Error if either value is missing, not a string, or out of length bounds
10
+ */
3
11
  export function anagram(value: string, value2: string): boolean {
4
12
  if (!value) throw new Error('First value is required');
5
13
  if (!value2) throw new Error('Second value is required');
@@ -16,6 +24,13 @@ export function anagram(value: string, value2: string): boolean {
16
24
  return value.split('').sort().join('') === value2.split('').sort().join('');
17
25
  }
18
26
 
27
+ /**
28
+ * Sorts a comma-separated list of numbers using the bubble sort algorithm.
29
+ *
30
+ * @param value - Comma-separated string of numbers (e.g. "3,1,2")
31
+ * @returns Array of numbers sorted in ascending order
32
+ * @throws Error if value is missing, non-numeric, or contains fewer than two elements
33
+ */
19
34
  export function bubblesort(value: string): number[] {
20
35
  if (!value) throw new Error('A value is required');
21
36
 
@@ -36,6 +51,13 @@ export function bubblesort(value: string): number[] {
36
51
  return arr;
37
52
  }
38
53
 
54
+ /**
55
+ * Computes the factorial of a non-negative integer.
56
+ *
57
+ * @param value - The integer to compute the factorial of
58
+ * @returns The factorial of the given number
59
+ * @throws Error if value is not a number, is negative, or exceeds the maximum allowed
60
+ */
39
61
  export function factorial(value: string | number): number {
40
62
  const num = Number(value);
41
63
  if (isNaN(num)) throw new Error('Value must be a number');
@@ -45,6 +67,13 @@ export function factorial(value: string | number): number {
45
67
  return num <= 1 ? 1 : num * factorial(num - 1);
46
68
  }
47
69
 
70
+ /**
71
+ * Returns the first n numbers of the Fibonacci sequence.
72
+ *
73
+ * @param value - How many Fibonacci numbers to generate
74
+ * @returns Array of Fibonacci numbers starting from 0
75
+ * @throws Error if value is not a number, is negative, or exceeds 1000
76
+ */
48
77
  export function fibonacci(value: string | number): number[] {
49
78
  const num = Number(value);
50
79
  if (isNaN(num)) throw new Error('Value must be a number');
@@ -60,6 +89,14 @@ export function fibonacci(value: string | number): number[] {
60
89
  return fib.slice(0, num);
61
90
  }
62
91
 
92
+ /**
93
+ * Computes the greatest common divisor of two positive integers using the Euclidean algorithm.
94
+ *
95
+ * @param value - First positive integer
96
+ * @param value2 - Second positive integer
97
+ * @returns The greatest common divisor
98
+ * @throws Error if either value is not a positive number or exceeds the maximum allowed
99
+ */
63
100
  export function gcd(value: string | number, value2: string | number): number {
64
101
  let a = Number(value);
65
102
  let b = Number(value2);
@@ -80,6 +117,13 @@ export function gcd(value: string | number, value2: string | number): number {
80
117
  return a;
81
118
  }
82
119
 
120
+ /**
121
+ * Determines whether a given number is prime.
122
+ *
123
+ * @param value - The number to test
124
+ * @returns True if the number is prime, false otherwise
125
+ * @throws Error if value is not a positive number or exceeds the maximum allowed
126
+ */
83
127
  export function isprime(value: string | number): boolean {
84
128
  const num = Number(value);
85
129
  if (isNaN(num)) throw new Error('Value must be a number');
@@ -93,6 +137,13 @@ export function isprime(value: string | number): boolean {
93
137
  return true;
94
138
  }
95
139
 
140
+ /**
141
+ * Checks whether a string reads the same forwards and backwards.
142
+ *
143
+ * @param value - The string to test
144
+ * @returns True if the string is a palindrome, false otherwise
145
+ * @throws Error if value is missing, not a string, or out of length bounds
146
+ */
96
147
  export function palindrome(value: string): boolean {
97
148
  if (!value) throw new Error('A value is required');
98
149
 
@@ -104,6 +155,13 @@ export function palindrome(value: string): boolean {
104
155
  return value === value.split('').reverse().join('');
105
156
  }
106
157
 
158
+ /**
159
+ * Returns the prime factorization of a given integer.
160
+ *
161
+ * @param value - An integer greater than 1
162
+ * @returns Array of prime factors in ascending order
163
+ * @throws Error if value is not a number, less than 2, or exceeds the maximum allowed
164
+ */
107
165
  export function primefactors(value: string | number): number[] {
108
166
  let num = Number(value);
109
167
  if (isNaN(num)) throw new Error('Value must be a number');
@@ -122,6 +180,13 @@ export function primefactors(value: string | number): number[] {
122
180
  return factors;
123
181
  }
124
182
 
183
+ /**
184
+ * Returns all prime numbers up to and including the given limit.
185
+ *
186
+ * @param value - The upper bound (inclusive)
187
+ * @returns Array of prime numbers up to the given limit
188
+ * @throws Error if value is not a number, less than 2, or exceeds the maximum allowed
189
+ */
125
190
  export function primelist(value: string | number): number[] {
126
191
  const num = Number(value);
127
192
  if (isNaN(num)) throw new Error('Value must be a number');
@@ -146,6 +211,13 @@ export function primelist(value: string | number): number[] {
146
211
  return primes;
147
212
  }
148
213
 
214
+ /**
215
+ * Reverses a string.
216
+ *
217
+ * @param value - The string to reverse
218
+ * @returns The reversed string
219
+ * @throws Error if value is missing or not a string
220
+ */
149
221
  export function reverse(value: string): string {
150
222
  if (!value) throw new Error('A value is required');
151
223
 
@@ -154,6 +226,13 @@ export function reverse(value: string): string {
154
226
  return value.split('').reverse().join('');
155
227
  }
156
228
 
229
+ /**
230
+ * Converts an integer to its Roman numeral representation, or a Roman numeral to an integer.
231
+ *
232
+ * @param value - An integer (1–3999) or a Roman numeral string
233
+ * @returns The converted value — a Roman numeral string if input was numeric, or an integer if input was a Roman numeral
234
+ * @throws Error if value is missing, out of range, or not a valid Roman numeral
235
+ */
157
236
  export function roman(value: string): number | string {
158
237
  if (!value) throw new Error('A value is required');
159
238
 
@@ -1,4 +1,5 @@
1
1
  import { createCanvas } from 'canvas';
2
+
2
3
  import { normalizeColor } from '../../utils/colors.js';
3
4
 
4
5
  export interface CaptchaOptions {
@@ -36,6 +37,13 @@ function clamp(value: number | undefined, name: string, def: number, min: number
36
37
  return Math.floor(value);
37
38
  }
38
39
 
40
+ /**
41
+ * Generates a CAPTCHA image with random or custom text and configurable noise.
42
+ *
43
+ * @param options - Captcha configuration options
44
+ * @returns Object containing the PNG buffer, content type, and the challenge text
45
+ * @throws Error if any option is out of the accepted range
46
+ */
39
47
  export default function captcha(options: CaptchaOptions): CaptchaResult {
40
48
  const text = options.text || generateText(clamp(options.length, 'length', 6, 1, 20));
41
49
  const height = clamp(options.height, 'height', 120, 50, 400);
@@ -1,6 +1,6 @@
1
- import { checkRateLimit } from '../../utils/helpers.js';
2
1
  import { SESSION_TTL } from '../../constants.js';
3
- import type { ChatStorage, ChatMessage } from '../../types/storage.js';
2
+ import type { ChatMessage, ChatStorage } from '../../types/storage.js';
3
+ import { checkRateLimit } from '../../utils/helpers.js';
4
4
 
5
5
  interface ChatParams {
6
6
  username: string;
@@ -11,6 +11,14 @@ interface ChatParams {
11
11
  storage: ChatStorage;
12
12
  }
13
13
 
14
+ /**
15
+ * Handles real-time chat actions including sending, fetching, and clearing messages.
16
+ *
17
+ * @param action - The action to perform: "message", "private", "fetch", or "clear"
18
+ * @param params - Chat parameters including username, message, session, and shared storage
19
+ * @returns The result of the action — a message list, a sent confirmation, or a status message
20
+ * @throws Error if a required parameter is missing or the action is invalid
21
+ */
14
22
  export default function chat(action: string, params: ChatParams): ChatMessage[] | ChatMessage | { message: string } {
15
23
  const storage = params.storage;
16
24
 
@@ -1,4 +1,4 @@
1
- import { hexToRgb, rgbToHsl, rgbToHex } from '../../utils/colors.js';
1
+ import { hexToRgb, rgbToHex, rgbToHsl } from '../../utils/colors.js';
2
2
 
3
3
  export interface ColorResult {
4
4
  hex: string;
@@ -41,6 +41,13 @@ function rgbToCmyk(r: number, g: number, b: number): [number, number, number, nu
41
41
  return [c * 100, m * 100, y * 100, k * 100];
42
42
  }
43
43
 
44
+ /**
45
+ * Converts a hex color to all major color space representations, or generates a random color.
46
+ *
47
+ * @param hex - Optional hex color string (e.g. "#ff5733"); generates a random color if omitted
48
+ * @returns Color in hex, RGB, HSL, HSV, HWB, and CMYK formats
49
+ * @throws Error if the hex string is invalid
50
+ */
44
51
  export default function color(hex?: string): ColorResult {
45
52
  let r: number, g: number, b: number;
46
53
 
@@ -97,6 +97,15 @@ const conversions: Record<string, Record<string, ConversionFn>> = {
97
97
  knots: { 'km/h': (v) => v * 1.852, mph: (v) => v * 1.15078, 'm/s': (v) => v * 0.514444 },
98
98
  };
99
99
 
100
+ /**
101
+ * Converts a numeric value from one unit to another.
102
+ *
103
+ * @param value - The numeric value to convert
104
+ * @param from - The source unit (e.g. "km", "celsius", "kg")
105
+ * @param to - The target unit (e.g. "mi", "fahrenheit", "lb")
106
+ * @returns Object with source unit, target unit, original value, and converted result
107
+ * @throws Error if the conversion pair is unsupported, the value is NaN, or the temperature is below absolute zero
108
+ */
100
109
  export default function convert(
101
110
  value: number,
102
111
  from: string,
@@ -7,6 +7,13 @@ export interface DiceResult {
7
7
  total: number;
8
8
  }
9
9
 
10
+ /**
11
+ * Rolls dice using standard tabletop notation and returns individual results with a total.
12
+ *
13
+ * @param roll - Dice notation string (e.g. "2d6+3", "d20", "4d8-1")
14
+ * @returns Roll breakdown including count, sides, modifier, individual results, and total
15
+ * @throws Error if the notation is missing, invalid, or out of bounds
16
+ */
10
17
  export default function dice(roll: string): DiceResult {
11
18
  if (!roll) throw new Error('A roll notation is required (e.g. 2d6+3)');
12
19
  if (typeof roll !== 'string') throw new Error('Roll must be a string');
@@ -1,5 +1,10 @@
1
- import { random, genIP } from '../../utils/helpers.js';
1
+ import { genIP, random } from '../../utils/helpers.js';
2
2
 
3
+ /**
4
+ * Generates a randomized fictional domain profile with metadata.
5
+ *
6
+ * @returns Object containing domain name, IP addresses, DNS info, SEO score, and other domain attributes
7
+ */
3
8
  export default function domain(): Record<string, unknown> {
4
9
  const subdomains = [
5
10
  'fr.',
@@ -66,22 +66,50 @@ function checkText(value: string): void {
66
66
  throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
67
67
  }
68
68
 
69
+ /**
70
+ * Encodes a UTF-8 string to Base64.
71
+ *
72
+ * @param value - The string to encode
73
+ * @returns Base64-encoded string
74
+ * @throws Error if value is missing, not a string, or too long
75
+ */
69
76
  export function base64encode(value: string): string {
70
77
  checkText(value);
71
78
  return Buffer.from(value, 'utf-8').toString('base64');
72
79
  }
73
80
 
81
+ /**
82
+ * Decodes a Base64 string to UTF-8.
83
+ *
84
+ * @param value - The Base64 string to decode
85
+ * @returns Decoded UTF-8 string
86
+ * @throws Error if value is missing, not valid Base64, or too long
87
+ */
74
88
  export function base64decode(value: string): string {
75
89
  checkText(value);
76
90
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) throw new Error('Invalid Base64 string');
77
91
  return Buffer.from(value, 'base64').toString('utf-8');
78
92
  }
79
93
 
94
+ /**
95
+ * Percent-encodes a string for safe use in a URL.
96
+ *
97
+ * @param value - The string to encode
98
+ * @returns URL-encoded string
99
+ * @throws Error if value is missing, not a string, or too long
100
+ */
80
101
  export function urlencode(value: string): string {
81
102
  checkText(value);
82
103
  return encodeURIComponent(value);
83
104
  }
84
105
 
106
+ /**
107
+ * Decodes a percent-encoded URL string.
108
+ *
109
+ * @param value - The URL-encoded string to decode
110
+ * @returns Decoded string
111
+ * @throws Error if value is missing, not a valid URL-encoded string, or too long
112
+ */
85
113
  export function urldecode(value: string): string {
86
114
  checkText(value);
87
115
  try {
@@ -91,6 +119,13 @@ export function urldecode(value: string): string {
91
119
  }
92
120
  }
93
121
 
122
+ /**
123
+ * Converts a plain text string to Morse code.
124
+ *
125
+ * @param value - The text to encode in Morse code
126
+ * @returns Morse code string where letters are separated by spaces and words by " / "
127
+ * @throws Error if value contains unsupported characters or is too long
128
+ */
94
129
  export function morse(value: string): string {
95
130
  checkText(value);
96
131
  return value
@@ -111,6 +146,13 @@ export function morse(value: string): string {
111
146
  .join(' / ');
112
147
  }
113
148
 
149
+ /**
150
+ * Converts a Morse code string back to plain text.
151
+ *
152
+ * @param value - Morse code string (letters separated by spaces, words by " / ")
153
+ * @returns Decoded plain text string
154
+ * @throws Error if the Morse code contains an unrecognized sequence or is too long
155
+ */
114
156
  export function unmorse(value: string): string {
115
157
  checkText(value);
116
158
  return value
@@ -129,6 +171,13 @@ export function unmorse(value: string): string {
129
171
  .join(' ');
130
172
  }
131
173
 
174
+ /**
175
+ * Applies the ROT13 substitution cipher to a string.
176
+ *
177
+ * @param value - The string to encode
178
+ * @returns ROT13-encoded string (applying it twice restores the original)
179
+ * @throws Error if value is missing, not a string, or too long
180
+ */
132
181
  export function rot13(value: string): string {
133
182
  checkText(value);
134
183
  return value.replace(/[a-zA-Z]/g, (c) => {
@@ -137,6 +186,14 @@ export function rot13(value: string): string {
137
186
  });
138
187
  }
139
188
 
189
+ /**
190
+ * Applies the Caesar cipher by shifting alphabetic characters by a given amount.
191
+ *
192
+ * @param value - The string to encode
193
+ * @param shift - Number of positions to shift (can be negative for left shift)
194
+ * @returns Caesar-shifted string
195
+ * @throws Error if value is missing or shift is not a number
196
+ */
140
197
  export function caesar(value: string, shift: string): string {
141
198
  checkText(value);
142
199
  const n = Number(shift);
@@ -148,6 +205,13 @@ export function caesar(value: string, shift: string): string {
148
205
  });
149
206
  }
150
207
 
208
+ /**
209
+ * Converts a string to its binary representation, one byte per character.
210
+ *
211
+ * @param value - The string to convert
212
+ * @returns Space-separated 8-bit binary groups (one per character)
213
+ * @throws Error if value is missing, not a string, or too long
214
+ */
151
215
  export function binary(value: string): string {
152
216
  checkText(value);
153
217
  return value
@@ -156,6 +220,13 @@ export function binary(value: string): string {
156
220
  .join(' ');
157
221
  }
158
222
 
223
+ /**
224
+ * Converts a binary string back to plain text.
225
+ *
226
+ * @param value - Space-separated 8-bit binary groups
227
+ * @returns Decoded string
228
+ * @throws Error if value contains non-binary characters or groups are not 8 bits wide
229
+ */
159
230
  export function unbinary(value: string): string {
160
231
  checkText(value);
161
232
  if (!/^[01\s]+$/.test(value)) throw new Error('Invalid binary string');
@@ -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;
@@ -27,6 +28,14 @@ function blocked(hostname: string): boolean {
27
28
  return false;
28
29
  }
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
+ */
30
39
  export default async function hyperplanning(url: string, detail?: string): Promise<CalendarEvent[]> {
31
40
  let parsed: URL;
32
41
  try {