@20syldev/api 4.3.0 → 4.5.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 (88) hide show
  1. package/README.md +3 -3
  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/config/versions.js +10 -0
  7. package/dist/config/versions.js.map +1 -1
  8. package/dist/constants.js +1 -2
  9. package/dist/constants.js.map +1 -1
  10. package/dist/middleware/cors.js +7 -2
  11. package/dist/middleware/cors.js.map +1 -1
  12. package/dist/middleware/error.js +3 -1
  13. package/dist/middleware/error.js.map +1 -1
  14. package/dist/middleware/ratelimit.js +39 -21
  15. package/dist/middleware/ratelimit.js.map +1 -1
  16. package/dist/modules/v4/captcha.js +60 -23
  17. package/dist/modules/v4/captcha.js.map +1 -1
  18. package/dist/modules/v4/chat.js +1 -1
  19. package/dist/modules/v4/chat.js.map +1 -1
  20. package/dist/modules/v4/color.js +44 -35
  21. package/dist/modules/v4/color.js.map +1 -1
  22. package/dist/modules/v4/colors.js +54 -0
  23. package/dist/modules/v4/colors.js.map +1 -0
  24. package/dist/modules/v4/convert.js +93 -19
  25. package/dist/modules/v4/convert.js.map +1 -1
  26. package/dist/modules/v4/domain.js +1 -1
  27. package/dist/modules/v4/domain.js.map +1 -1
  28. package/dist/modules/v4/hash.js +11 -4
  29. package/dist/modules/v4/hash.js.map +1 -1
  30. package/dist/modules/v4/hyperplanning.js +32 -2
  31. package/dist/modules/v4/hyperplanning.js.map +1 -1
  32. package/dist/modules/v4/palette.js +2 -43
  33. package/dist/modules/v4/palette.js.map +1 -1
  34. package/dist/modules/v4/personal.js +1 -1
  35. package/dist/modules/v4/personal.js.map +1 -1
  36. package/dist/modules/v4/placeholder.js +5 -10
  37. package/dist/modules/v4/placeholder.js.map +1 -1
  38. package/dist/modules/v4/qrcode.js +79 -6
  39. package/dist/modules/v4/qrcode.js.map +1 -1
  40. package/dist/modules/v4/username.js +1 -1
  41. package/dist/modules/v4/username.js.map +1 -1
  42. package/dist/routes/delete.js +14 -14
  43. package/dist/routes/delete.js.map +1 -1
  44. package/dist/routes/get.js +73 -17
  45. package/dist/routes/get.js.map +1 -1
  46. package/dist/routes/post.js +11 -3
  47. package/dist/routes/post.js.map +1 -1
  48. package/dist/utils/colors.js +93 -0
  49. package/dist/utils/colors.js.map +1 -0
  50. package/dist/utils/helpers.js +77 -0
  51. package/dist/utils/helpers.js.map +1 -0
  52. package/dist/utils/response.js +18 -2
  53. package/dist/utils/response.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/config/env.ts +6 -1
  56. package/src/config/plans.ts +61 -0
  57. package/src/config/versions.ts +10 -0
  58. package/src/constants.ts +1 -2
  59. package/src/middleware/cors.ts +8 -2
  60. package/src/middleware/error.ts +10 -2
  61. package/src/middleware/ratelimit.ts +42 -20
  62. package/src/modules/v4/captcha.ts +84 -27
  63. package/src/modules/v4/chat.ts +1 -1
  64. package/src/modules/v4/color.ts +56 -52
  65. package/src/modules/v4/convert.ts +100 -20
  66. package/src/modules/v4/domain.ts +1 -1
  67. package/src/modules/v4/hash.ts +18 -4
  68. package/src/modules/v4/hyperplanning.ts +28 -2
  69. package/src/modules/v4/palette.ts +3 -43
  70. package/src/modules/v4/personal.ts +1 -1
  71. package/src/modules/v4/placeholder.ts +6 -9
  72. package/src/modules/v4/qrcode.ts +108 -6
  73. package/src/modules/v4/username.ts +1 -1
  74. package/src/routes/get.ts +75 -19
  75. package/src/routes/post.ts +11 -3
  76. package/src/utils/colors.ts +91 -0
  77. package/src/utils/helpers.ts +90 -0
  78. package/src/utils/response.ts +18 -2
  79. package/tests/integration/api.test.ts +89 -7
  80. package/tests/unit/captcha.test.ts +50 -12
  81. package/tests/unit/chat.test.ts +4 -1
  82. package/tests/unit/color.test.ts +24 -4
  83. package/tests/unit/convert.test.ts +26 -1
  84. package/tests/unit/hash.test.ts +17 -10
  85. package/tests/unit/hyperplanning.test.ts +19 -0
  86. package/tests/unit/qrcode.test.ts +72 -4
  87. package/tests/unit/tic_tac_toe.test.ts +4 -1
  88. package/src/modules/v4/utils.ts +0 -38
@@ -1,38 +1,118 @@
1
- import { MIN_TEMPERATURE, MAX_TEMPERATURE } from '../../constants.js';
2
-
3
1
  type ConversionFn = (val: number) => number;
4
2
 
3
+ const TEMPERATURE_UNITS = new Set(['celsius', 'fahrenheit', 'kelvin']);
4
+ const ABSOLUTE_ZERO: Record<string, number> = { celsius: -273.15, fahrenheit: -459.67, kelvin: 0 };
5
+
5
6
  const conversions: Record<string, Record<string, ConversionFn>> = {
7
+ // Temperature
6
8
  celsius: {
7
- fahrenheit: (val) => (val * 9) / 5 + 32,
8
- kelvin: (val) => val + 273.15,
9
+ fahrenheit: (v) => (v * 9) / 5 + 32,
10
+ kelvin: (v) => v + 273.15,
9
11
  },
10
12
  fahrenheit: {
11
- celsius: (val) => ((val - 32) * 5) / 9,
12
- kelvin: (val) => ((val - 32) * 5) / 9 + 273.15,
13
+ celsius: (v) => ((v - 32) * 5) / 9,
14
+ kelvin: (v) => ((v - 32) * 5) / 9 + 273.15,
13
15
  },
14
16
  kelvin: {
15
- celsius: (val) => val - 273.15,
16
- fahrenheit: (val) => ((val - 273.15) * 9) / 5 + 32,
17
+ celsius: (v) => v - 273.15,
18
+ fahrenheit: (v) => ((v - 273.15) * 9) / 5 + 32,
19
+ },
20
+
21
+ // Length
22
+ km: {
23
+ mi: (v) => v * 0.621371,
24
+ m: (v) => v * 1000,
25
+ ft: (v) => v * 3280.84,
26
+ cm: (v) => v * 100000,
27
+ in: (v) => v * 39370.1,
28
+ yd: (v) => v * 1093.61,
29
+ },
30
+ mi: {
31
+ km: (v) => v * 1.60934,
32
+ m: (v) => v * 1609.34,
33
+ ft: (v) => v * 5280,
34
+ cm: (v) => v * 160934,
35
+ in: (v) => v * 63360,
36
+ yd: (v) => v * 1760,
37
+ },
38
+ m: {
39
+ km: (v) => v / 1000,
40
+ mi: (v) => v * 0.000621371,
41
+ ft: (v) => v * 3.28084,
42
+ cm: (v) => v * 100,
43
+ in: (v) => v * 39.3701,
44
+ yd: (v) => v * 1.09361,
45
+ },
46
+ ft: {
47
+ km: (v) => v * 0.0003048,
48
+ mi: (v) => v / 5280,
49
+ m: (v) => v * 0.3048,
50
+ cm: (v) => v * 30.48,
51
+ in: (v) => v * 12,
52
+ yd: (v) => v / 3,
53
+ },
54
+ cm: {
55
+ km: (v) => v / 100000,
56
+ mi: (v) => v / 160934,
57
+ m: (v) => v / 100,
58
+ ft: (v) => v / 30.48,
59
+ in: (v) => v / 2.54,
60
+ yd: (v) => v / 91.44,
17
61
  },
62
+ in: {
63
+ km: (v) => v / 39370.1,
64
+ mi: (v) => v / 63360,
65
+ m: (v) => v * 0.0254,
66
+ ft: (v) => v / 12,
67
+ cm: (v) => v * 2.54,
68
+ yd: (v) => v / 36,
69
+ },
70
+ yd: {
71
+ km: (v) => v * 0.0009144,
72
+ mi: (v) => v / 1760,
73
+ m: (v) => v * 0.9144,
74
+ ft: (v) => v * 3,
75
+ cm: (v) => v * 91.44,
76
+ in: (v) => v * 36,
77
+ },
78
+
79
+ // Weight
80
+ kg: { lb: (v) => v * 2.20462, oz: (v) => v * 35.274, g: (v) => v * 1000, ton: (v) => v / 1000 },
81
+ lb: { kg: (v) => v * 0.453592, oz: (v) => v * 16, g: (v) => v * 453.592, ton: (v) => v * 0.000453592 },
82
+ oz: { kg: (v) => v * 0.0283495, lb: (v) => v / 16, g: (v) => v * 28.3495, ton: (v) => v * 0.0000283495 },
83
+ g: { kg: (v) => v / 1000, lb: (v) => v * 0.00220462, oz: (v) => v * 0.035274, ton: (v) => v / 1000000 },
84
+ ton: { kg: (v) => v * 1000, lb: (v) => v * 2204.62, oz: (v) => v * 35274, g: (v) => v * 1000000 },
85
+
86
+ // Data
87
+ b: { kb: (v) => v / 1024, mb: (v) => v / 1048576, gb: (v) => v / 1073741824, tb: (v) => v / 1099511627776 },
88
+ kb: { b: (v) => v * 1024, mb: (v) => v / 1024, gb: (v) => v / 1048576, tb: (v) => v / 1073741824 },
89
+ mb: { b: (v) => v * 1048576, kb: (v) => v * 1024, gb: (v) => v / 1024, tb: (v) => v / 1048576 },
90
+ gb: { b: (v) => v * 1073741824, kb: (v) => v * 1048576, mb: (v) => v * 1024, tb: (v) => v / 1024 },
91
+ tb: { b: (v) => v * 1099511627776, kb: (v) => v * 1073741824, mb: (v) => v * 1048576, gb: (v) => v * 1024 },
92
+
93
+ // Speed
94
+ 'km/h': { mph: (v) => v * 0.621371, 'm/s': (v) => v / 3.6, knots: (v) => v * 0.539957 },
95
+ mph: { 'km/h': (v) => v * 1.60934, 'm/s': (v) => v * 0.44704, knots: (v) => v * 0.868976 },
96
+ 'm/s': { 'km/h': (v) => v * 3.6, mph: (v) => v * 2.23694, knots: (v) => v * 1.94384 },
97
+ knots: { 'km/h': (v) => v * 1.852, mph: (v) => v * 1.15078, 'm/s': (v) => v * 0.514444 },
18
98
  };
19
99
 
20
100
  export default function convert(
21
- value: string | number,
101
+ value: number,
22
102
  from: string,
23
103
  to: string,
24
104
  ): { from: string; to: string; value: number; result: number } {
25
- const convertFn = conversions[from.toLowerCase()]?.[to.toLowerCase()];
105
+ const fromKey = from.toLowerCase();
106
+ const toKey = to.toLowerCase();
107
+ const convertFn = conversions[fromKey]?.[toKey];
26
108
 
27
109
  if (!convertFn) throw new Error('Invalid conversion unit');
28
- if (isNaN(Number(value))) throw new Error('Value must be a number');
29
- if (Number(value) < MIN_TEMPERATURE) throw new Error('Value must be greater than absolute zero');
30
- if (Number(value) > MAX_TEMPERATURE) throw new Error('Value must be less than 1,000,000');
31
-
32
- return {
33
- from,
34
- to,
35
- value: parseFloat(String(value)),
36
- result: convertFn(parseFloat(String(value))),
37
- };
110
+ if (isNaN(value)) throw new Error('Value must be a number');
111
+
112
+ if (TEMPERATURE_UNITS.has(fromKey)) {
113
+ const minValue = ABSOLUTE_ZERO[fromKey]!;
114
+ if (value < minValue) throw new Error('Value must be greater than absolute zero');
115
+ }
116
+
117
+ return { from, to, value, result: convertFn(value) };
38
118
  }
@@ -1,4 +1,4 @@
1
- import { random, genIP } from './utils.js';
1
+ import { random, genIP } from '../../utils/helpers.js';
2
2
 
3
3
  export default function domain(): Record<string, unknown> {
4
4
  const subdomains = [
@@ -1,9 +1,23 @@
1
1
  import { getHashes, createHash } from 'crypto';
2
2
 
3
- export default function hash(text: string, method: string): { method: string; hash: string } | { error: string } {
3
+ export interface HashResult {
4
+ method: string;
5
+ hash: string;
6
+ encoding: string;
7
+ }
8
+
9
+ const ENCODINGS = new Set(['hex', 'base64']);
10
+
11
+ export default function hash(text: string, method: string, encoding: string = 'hex'): HashResult {
12
+ if (!text) throw new Error('Text is required');
13
+
4
14
  const methods = getHashes();
5
- if (!methods.includes(method)) return { error: `Unsupported method. Use one of: ${methods.join(', ')}` };
15
+ if (!methods.includes(method)) throw new Error(`Unsupported method. Use one of: ${methods.join(', ')}`);
16
+
17
+ if (!ENCODINGS.has(encoding)) throw new Error('Encoding must be one of: hex, base64');
6
18
 
7
- const result = createHash(method).update(text).digest('hex');
8
- return { method, hash: result };
19
+ const result = createHash(method)
20
+ .update(text)
21
+ .digest(encoding as 'hex' | 'base64');
22
+ return { method, hash: result, encoding };
9
23
  }
@@ -1,4 +1,4 @@
1
- import { formatDate } from './utils.js';
1
+ import { formatDate } from '../../utils/helpers.js';
2
2
  import ical from 'ical.js';
3
3
 
4
4
  interface CalendarEvent {
@@ -11,8 +11,34 @@ interface CalendarEvent {
11
11
  end: string;
12
12
  }
13
13
 
14
+ function blocked(hostname: string): boolean {
15
+ const list = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]', 'metadata.google.internal'];
16
+ if (list.includes(hostname)) return true;
17
+
18
+ const parts = hostname.split('.').map(Number);
19
+ if (parts.length === 4 && parts.every((n) => !isNaN(n))) {
20
+ if (parts[0] === 10) return true;
21
+ if (parts[0] === 172 && parts[1]! >= 16 && parts[1]! <= 31) return true;
22
+ if (parts[0] === 192 && parts[1] === 168) return true;
23
+ if (parts[0] === 169 && parts[1] === 254) return true;
24
+ if (parts[0] === 0) return true;
25
+ }
26
+
27
+ return false;
28
+ }
29
+
14
30
  export default async function hyperplanning(url: string, detail?: string): Promise<CalendarEvent[]> {
15
- const response = await fetch(url);
31
+ let parsed: URL;
32
+ try {
33
+ parsed = new URL(url);
34
+ } catch {
35
+ throw new Error('Invalid URL.');
36
+ }
37
+
38
+ if (parsed.protocol !== 'https:') throw new Error('Only HTTPS URLs are allowed.');
39
+ if (blocked(parsed.hostname)) throw new Error('Access to private/internal hosts is not allowed.');
40
+
41
+ const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
16
42
 
17
43
  if (!response.ok || !(response.headers.get('content-type') || '').includes('text/calendar')) {
18
44
  throw new Error('Invalid ICS file format.');
@@ -1,3 +1,5 @@
1
+ import { hexToRgb, rgbToHsl, hslToRgb, rgbToHex } from '../../utils/colors.js';
2
+
1
3
  export interface PaletteColor {
2
4
  hex: string;
3
5
  rgb: string;
@@ -10,52 +12,10 @@ export interface PaletteResult {
10
12
  colors: PaletteColor[];
11
13
  }
12
14
 
13
- function hexToRgb(hex: string): [number, number, number] {
14
- const clean = hex.replace('#', '');
15
- if (!/^[0-9a-fA-F]{6}$/.test(clean)) throw new Error('Invalid HEX color (use #RRGGBB)');
16
- return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];
17
- }
18
-
19
- function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
20
- const r1 = r / 255,
21
- g1 = g / 255,
22
- b1 = b / 255;
23
- const max = Math.max(r1, g1, b1),
24
- min = Math.min(r1, g1, b1);
25
- const l = (max + min) / 2;
26
-
27
- if (max === min) return [0, 0, l];
28
-
29
- const d = max - min;
30
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
31
- let h = 0;
32
- if (max === r1) h = (g1 - b1) / d + (g1 < b1 ? 6 : 0);
33
- else if (max === g1) h = (b1 - r1) / d + 2;
34
- else h = (r1 - g1) / d + 4;
35
-
36
- return [(h * 60 + 360) % 360, s, l];
37
- }
38
-
39
- function hslToRgb(h: number, s: number, l: number): [number, number, number] {
40
- const c = (1 - Math.abs(2 * l - 1)) * s;
41
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
42
- const m = l - c / 2;
43
- let r1 = 0,
44
- g1 = 0,
45
- b1 = 0;
46
- if (h < 60) [r1, g1, b1] = [c, x, 0];
47
- else if (h < 120) [r1, g1, b1] = [x, c, 0];
48
- else if (h < 180) [r1, g1, b1] = [0, c, x];
49
- else if (h < 240) [r1, g1, b1] = [0, x, c];
50
- else if (h < 300) [r1, g1, b1] = [x, 0, c];
51
- else [r1, g1, b1] = [c, 0, x];
52
- return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
53
- }
54
-
55
15
  function format(r: number, g: number, b: number): PaletteColor {
56
16
  const [h, s, l] = rgbToHsl(r, g, b);
57
17
  return {
58
- hex: `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`,
18
+ hex: rgbToHex(r, g, b),
59
19
  rgb: `rgb(${r}, ${g}, ${b})`,
60
20
  hsl: `hsl(${h.toFixed(1)}, ${(s * 100).toFixed(1)}%, ${(l * 100).toFixed(1)}%)`,
61
21
  };
@@ -1,4 +1,4 @@
1
- import { random, randomNumber } from './utils.js';
1
+ import { random, randomNumber } from '../../utils/helpers.js';
2
2
 
3
3
  interface Person {
4
4
  name: string;
@@ -1,3 +1,5 @@
1
+ import { normalizeColor } from '../../utils/colors.js';
2
+
1
3
  type AvatarShape = 'circle' | 'rounded' | 'square';
2
4
  type AnimateMode = 'shimmer' | 'pulse' | 'none';
3
5
 
@@ -23,13 +25,6 @@ function parseSize(value: string | undefined, name: string, def: number): number
23
25
  return Math.floor(n);
24
26
  }
25
27
 
26
- function normalizeColor(value: string | undefined, def: string): string {
27
- if (!value) return def;
28
- const clean = value.startsWith('#') ? value : `#${value}`;
29
- if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(clean)) throw new Error('Invalid color (use hex like #ff6600)');
30
- return clean;
31
- }
32
-
33
28
  function escapeXml(str: string): string {
34
29
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
35
30
  }
@@ -88,8 +83,10 @@ function generateSkeleton(opts: PlaceholderOptions): string {
88
83
  const rx = opts.radius ?? 4;
89
84
 
90
85
  const pad = Math.round(width * 0.06);
91
- const gap = Math.round(Math.max(6, Math.min(14, height * 0.04)));
92
- const lineH = Math.round(Math.max(8, Math.min(16, ((height - pad * 2) / (rows + (avatar ? 4 : 0))) * 0.65)));
86
+ const slots = rows + (avatar ? 4 : 0);
87
+ const available = height - pad * 2;
88
+ const lineH = Math.round(Math.max(8, (available / Math.max(1, slots)) * 0.65));
89
+ const gap = Math.round(Math.max(6, (available / Math.max(1, slots)) * 0.35));
93
90
 
94
91
  const shapes: string[] = [];
95
92
  let y = pad;
@@ -1,11 +1,113 @@
1
- import { toDataURL } from 'qrcode';
1
+ import { toBuffer, toDataURL, toCanvas as qrToCanvas } from 'qrcode';
2
+ import { createCanvas, loadImage } from 'canvas';
3
+ import { normalizeColor } from '../../utils/colors.js';
2
4
 
3
- export default async function qrcode(url: string): Promise<string> {
5
+ export interface QRCodeOptions {
6
+ url: string;
7
+ size?: number;
8
+ margin?: number;
9
+ correction?: 'L' | 'M' | 'Q' | 'H';
10
+ dark?: string;
11
+ light?: string;
12
+ icon?: string;
13
+ iconSize?: number;
14
+ iconPadding?: number;
15
+ iconRadius?: number;
16
+ format?: 'png' | 'base64';
17
+ }
18
+
19
+ export interface QRCodeResult {
20
+ contentType: string;
21
+ body: Buffer | string;
22
+ }
23
+
24
+ const CORRECTIONS = new Set(['L', 'M', 'Q', 'H']);
25
+ const FORMATS = new Set(['png', 'base64']);
26
+ const MAX_LOGO_BYTES = 2 * 1024 * 1024;
27
+
28
+ function clamp(value: number | undefined, name: string, def: number, min: number, max: number): number {
29
+ if (value === undefined) return def;
30
+ if (isNaN(value)) throw new Error(`${name} must be a number`);
31
+ if (value < min || value > max) throw new Error(`${name} must be between ${min} and ${max}`);
32
+ return Math.floor(value);
33
+ }
34
+
35
+ async function fetchIcon(url: string): Promise<Buffer> {
36
+ if (!/^https:\/\//i.test(url)) throw new Error('Icon URL must use HTTPS');
37
+
38
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
39
+
40
+ if (!response.ok) throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
41
+
42
+ const contentType = response.headers.get('content-type') ?? '';
43
+ if (!contentType.startsWith('image/')) throw new Error('Icon URL must point to an image');
44
+
45
+ const buffer = Buffer.from(await response.arrayBuffer());
46
+ if (buffer.length > MAX_LOGO_BYTES) throw new Error('Icon must be under 2MB');
47
+
48
+ return buffer;
49
+ }
50
+
51
+ export default async function qrcode(options: QRCodeOptions): Promise<QRCodeResult> {
52
+ const { url } = options;
4
53
  if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
5
54
 
6
- try {
7
- return await toDataURL(url);
8
- } catch (error) {
9
- throw new Error(`Failed to generate QR code: ${(error as Error).message}`);
55
+ const format = options.format ?? 'png';
56
+ if (!FORMATS.has(format)) throw new Error('format must be one of: png, base64');
57
+
58
+ const size = clamp(options.size, 'size', 200, 50, 2000);
59
+ const margin = clamp(options.margin, 'margin', 4, 0, 10);
60
+ const dark = normalizeColor(options.dark, '#000000');
61
+ const light = normalizeColor(options.light, '#ffffff');
62
+
63
+ let correction = options.correction ?? 'M';
64
+ if (!CORRECTIONS.has(correction)) throw new Error('correction must be one of: L, M, Q, H');
65
+ if (options.icon) correction = 'H';
66
+
67
+ const qrOpts = {
68
+ width: size,
69
+ margin,
70
+ errorCorrectionLevel: correction as 'L' | 'M' | 'Q' | 'H',
71
+ color: { dark, light },
72
+ };
73
+
74
+ if (!options.icon) {
75
+ if (format === 'base64') {
76
+ return { contentType: 'application/json', body: await toDataURL(url, qrOpts) };
77
+ }
78
+ return { contentType: 'image/png', body: await toBuffer(url, qrOpts) };
79
+ }
80
+
81
+ const canvas = createCanvas(size, size);
82
+ await qrToCanvas(canvas as unknown as HTMLCanvasElement, url, qrOpts);
83
+
84
+ const iconBuffer = await fetchIcon(options.icon);
85
+ const iconImg = await loadImage(iconBuffer);
86
+
87
+ const iconSize = clamp(options.iconSize, 'iconSize', Math.round(size * 0.2), 10, Math.round(size * 0.4));
88
+ const iconPadding = clamp(options.iconPadding, 'iconPadding', 5, 0, 50);
89
+ const maxRadius = Math.floor((iconSize + iconPadding * 2) / 2);
90
+ const iconRadius = clamp(options.iconRadius, 'iconRadius', 0, 0, maxRadius);
91
+
92
+ const ctx = canvas.getContext('2d');
93
+ const totalSize = iconSize + iconPadding * 2;
94
+ const x = (canvas.width - totalSize) / 2;
95
+ const y = (canvas.height - totalSize) / 2;
96
+
97
+ ctx.beginPath();
98
+ ctx.roundRect(x, y, totalSize, totalSize, iconRadius);
99
+ ctx.fillStyle = light;
100
+ ctx.fill();
101
+
102
+ ctx.save();
103
+ ctx.beginPath();
104
+ ctx.roundRect(x + iconPadding, y + iconPadding, iconSize, iconSize, Math.max(0, iconRadius - iconPadding));
105
+ ctx.clip();
106
+ ctx.drawImage(iconImg, x + iconPadding, y + iconPadding, iconSize, iconSize);
107
+ ctx.restore();
108
+
109
+ if (format === 'base64') {
110
+ return { contentType: 'application/json', body: canvas.toDataURL('image/png') };
10
111
  }
112
+ return { contentType: 'image/png', body: canvas.toBuffer('image/png') };
11
113
  }
@@ -1,4 +1,4 @@
1
- import { random, randomNumber } from './utils.js';
1
+ import { random, randomNumber } from '../../utils/helpers.js';
2
2
 
3
3
  export default function username(): Record<string, unknown> {
4
4
  const adj = [
package/src/routes/get.ts CHANGED
@@ -5,6 +5,10 @@ import { ipLimits } from '../storage/index.js';
5
5
  import { chatStorage } from '../storage/index.js';
6
6
  import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
7
7
  import { error } from '../utils/response.js';
8
+ import { since } from '../utils/helpers.js';
9
+ import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
10
+ import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
11
+ import type { ColorResult } from '../modules/v4/color.js';
8
12
 
9
13
  const router = Router();
10
14
 
@@ -57,7 +61,7 @@ router.get('/:version/algorithms', (req: Request, res: Response) => {
57
61
  const { version } = req.params;
58
62
 
59
63
  const algorithms = req.module.algorithms as Record<string, (v: string, v2?: string) => unknown>;
60
- if (!algorithms || !algorithms[method as string]) {
64
+ if (!algorithms || !method || !Object.hasOwn(algorithms, method as string)) {
61
65
  error(res, 400, 'Please provide a valid algorithm (?method={algorithm})', `${version}/algorithms`);
62
66
  return;
63
67
  }
@@ -72,16 +76,29 @@ router.get('/:version/algorithms', (req: Request, res: Response) => {
72
76
 
73
77
  // Generate captcha
74
78
  router.get('/:version/captcha', (req: Request, res: Response) => {
75
- const text = req.query.text as string;
76
-
77
- if (!text) {
78
- error(res, 400, 'Please provide a valid argument (?text={text})', `${req.version}/captcha`);
79
- return;
80
- }
81
-
82
79
  try {
83
- const result = req.module.captcha(text);
84
- res.type('png').send(result);
80
+ if (since(req.version, 4)) {
81
+ const captchaFn = req.module.captcha as (o: CaptchaOptions) => CaptchaResult;
82
+ const result = captchaFn({
83
+ text: req.query.text as string | undefined,
84
+ length: req.query.length ? Number(req.query.length) : undefined,
85
+ width: req.query.width ? Number(req.query.width) : undefined,
86
+ height: req.query.height ? Number(req.query.height) : undefined,
87
+ noise: req.query.noise as CaptchaOptions['noise'],
88
+ bg: req.query.bg as string | undefined,
89
+ color: req.query.color as string | undefined,
90
+ });
91
+ res.set('X-Captcha-Text', result.text);
92
+ res.type('png').send(result.body);
93
+ } else {
94
+ const text = req.query.text as string;
95
+ if (!text) {
96
+ error(res, 400, 'Please provide a valid argument (?text={text})', `${req.version}/captcha`);
97
+ return;
98
+ }
99
+ const result = (req.module.captcha as (t: string) => Buffer)(text);
100
+ res.type('png').send(result);
101
+ }
85
102
  } catch (err) {
86
103
  error(res, 400, (err as Error).message, `${req.version}/captcha`);
87
104
  }
@@ -108,8 +125,15 @@ router.get('/:version/chat/private', (_req: Request, res: Response) => {
108
125
  // Generate color
109
126
  router.get('/:version/color', (req: Request, res: Response) => {
110
127
  try {
111
- const result = req.module.color();
112
- res.jsonResponse(result);
128
+ if (since(req.version, 4)) {
129
+ const colorFn = req.module.color as (hex?: string) => ColorResult;
130
+ const hex = req.query.hex as string | undefined;
131
+ const result = colorFn(hex || undefined);
132
+ res.jsonResponse(result);
133
+ } else {
134
+ const result = (req.module.color as () => Record<string, string>)();
135
+ res.jsonResponse(result);
136
+ }
113
137
  } catch (err) {
114
138
  error(res, 400, (err as Error).message, `${req.version}/color`);
115
139
  }
@@ -133,8 +157,18 @@ router.get('/:version/convert', (req: Request, res: Response) => {
133
157
  }
134
158
 
135
159
  try {
136
- const result = req.module.convert(value as string, from as string, to as string);
137
- res.jsonResponse(result);
160
+ if (since(req.version, 4)) {
161
+ const convertFn = req.module.convert as (v: number, f: string, t: string) => Record<string, unknown>;
162
+ const result = convertFn(Number(value), from as string, to as string);
163
+ res.jsonResponse(result);
164
+ } else {
165
+ const result = (req.module.convert as (v: string, f: string, t: string) => Record<string, unknown>)(
166
+ value as string,
167
+ from as string,
168
+ to as string,
169
+ );
170
+ res.jsonResponse(result);
171
+ }
138
172
  } catch (err) {
139
173
  error(res, 400, (err as Error).message, `${req.version}/convert`);
140
174
  }
@@ -183,7 +217,7 @@ router.get('/:version/encode', (req: Request, res: Response) => {
183
217
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/encode`);
184
218
  return;
185
219
  }
186
- if (!method || !encode[method as string]) {
220
+ if (!method || !Object.hasOwn(encode, method as string)) {
187
221
  error(res, 400, 'Please provide a valid method (?method={method})', `${version}/encode`);
188
222
  return;
189
223
  }
@@ -338,8 +372,30 @@ router.get('/:version/qrcode', async (req: Request, res: Response) => {
338
372
  }
339
373
 
340
374
  try {
341
- const result = await req.module.qrcode(url as string);
342
- res.jsonResponse(result);
375
+ if (since(req.version, 4)) {
376
+ const qrcodeFn = req.module.qrcode as (o: QRCodeOptions) => Promise<QRCodeResult>;
377
+ const result = await qrcodeFn({
378
+ url: url as string,
379
+ size: req.query.size ? Number(req.query.size) : undefined,
380
+ margin: req.query.margin ? Number(req.query.margin) : undefined,
381
+ correction: req.query.correction as QRCodeOptions['correction'],
382
+ dark: req.query.dark as string | undefined,
383
+ light: req.query.light as string | undefined,
384
+ icon: req.query.icon as string | undefined,
385
+ iconSize: req.query.iconSize ? Number(req.query.iconSize) : undefined,
386
+ iconPadding: req.query.iconPadding ? Number(req.query.iconPadding) : undefined,
387
+ iconRadius: req.query.iconRadius ? Number(req.query.iconRadius) : undefined,
388
+ format: req.query.format as QRCodeOptions['format'],
389
+ });
390
+ if (result.contentType === 'application/json') {
391
+ res.jsonResponse(result.body);
392
+ } else {
393
+ res.type(result.contentType).send(result.body);
394
+ }
395
+ } else {
396
+ const result = await (req.module.qrcode as (u: string) => Promise<string>)(url as string);
397
+ res.jsonResponse(result);
398
+ }
343
399
  } catch (err) {
344
400
  error(res, 400, (err as Error).message, `${req.version}/qrcode`);
345
401
  }
@@ -378,7 +434,7 @@ router.get('/:version/text', (req: Request, res: Response) => {
378
434
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/text`);
379
435
  return;
380
436
  }
381
- if (!method || !textMod[method as string]) {
437
+ if (!method || !Object.hasOwn(textMod, method as string)) {
382
438
  error(res, 400, 'Please provide a valid method (?method={slug|stats|lorem|number})', `${version}/text`);
383
439
  return;
384
440
  }
@@ -415,7 +471,7 @@ router.get('/:version/validate', (req: Request, res: Response) => {
415
471
  error(res, 404, `Endpoint not available in ${version}.`, `${version}/validate`);
416
472
  return;
417
473
  }
418
- if (!type || !validate[type as string]) {
474
+ if (!type || !Object.hasOwn(validate, type as string)) {
419
475
  error(res, 400, 'Please provide a valid type (?type={luhn|iban|email})', `${version}/validate`);
420
476
  return;
421
477
  }
@@ -2,6 +2,8 @@ import { Router, type Request, type Response } from 'express';
2
2
  import { chatStorage, ticTacToeStorage } from '../storage/index.js';
3
3
  import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../constants.js';
4
4
  import { error } from '../utils/response.js';
5
+ import { since } from '../utils/helpers.js';
6
+ import type { HashResult } from '../modules/v4/hash.js';
5
7
 
6
8
  const router = Router();
7
9
 
@@ -64,7 +66,7 @@ router.post('/:version/chat/private', (req: Request, res: Response) => {
64
66
 
65
67
  // Generate hash
66
68
  router.post('/:version/hash', (req: Request, res: Response) => {
67
- const { text, method } = (req.body as Record<string, string>) || {};
69
+ const { text, method, encoding } = (req.body as Record<string, string>) || {};
68
70
 
69
71
  if (!text) {
70
72
  error(res, 400, 'Please provide a text (?text={text})', `${req.version}/hash`);
@@ -76,8 +78,14 @@ router.post('/:version/hash', (req: Request, res: Response) => {
76
78
  }
77
79
 
78
80
  try {
79
- const result = req.module.hash(text, method);
80
- res.jsonResponse(result);
81
+ if (since(req.version, 4)) {
82
+ const hashFn = req.module.hash as (t: string, m: string, e?: string) => HashResult;
83
+ const result = hashFn(text, method, encoding);
84
+ res.jsonResponse(result);
85
+ } else {
86
+ const result = (req.module.hash as (t: string, m: string) => Record<string, string>)(text, method);
87
+ res.jsonResponse(result);
88
+ }
81
89
  } catch (err) {
82
90
  error(res, 400, (err as Error).message, `${req.version}/hash`);
83
91
  }