@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.
- package/README.md +3 -3
- package/dist/config/env.js +5 -1
- package/dist/config/env.js.map +1 -1
- package/dist/config/plans.js +44 -0
- package/dist/config/plans.js.map +1 -0
- package/dist/config/versions.js +10 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +1 -2
- package/dist/constants.js.map +1 -1
- package/dist/middleware/cors.js +7 -2
- package/dist/middleware/cors.js.map +1 -1
- package/dist/middleware/error.js +3 -1
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/ratelimit.js +39 -21
- package/dist/middleware/ratelimit.js.map +1 -1
- package/dist/modules/v4/captcha.js +60 -23
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +1 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +44 -35
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/colors.js +54 -0
- package/dist/modules/v4/colors.js.map +1 -0
- package/dist/modules/v4/convert.js +93 -19
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/domain.js +1 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/hash.js +11 -4
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +32 -2
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/dist/modules/v4/palette.js +2 -43
- package/dist/modules/v4/palette.js.map +1 -1
- package/dist/modules/v4/personal.js +1 -1
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +5 -10
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +79 -6
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/username.js +1 -1
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/routes/delete.js +14 -14
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +73 -17
- package/dist/routes/get.js.map +1 -1
- package/dist/routes/post.js +11 -3
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/colors.js +93 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/helpers.js +77 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/response.js +18 -2
- package/dist/utils/response.js.map +1 -1
- package/package.json +1 -1
- package/src/config/env.ts +6 -1
- package/src/config/plans.ts +61 -0
- package/src/config/versions.ts +10 -0
- package/src/constants.ts +1 -2
- package/src/middleware/cors.ts +8 -2
- package/src/middleware/error.ts +10 -2
- package/src/middleware/ratelimit.ts +42 -20
- package/src/modules/v4/captcha.ts +84 -27
- package/src/modules/v4/chat.ts +1 -1
- package/src/modules/v4/color.ts +56 -52
- package/src/modules/v4/convert.ts +100 -20
- package/src/modules/v4/domain.ts +1 -1
- package/src/modules/v4/hash.ts +18 -4
- package/src/modules/v4/hyperplanning.ts +28 -2
- package/src/modules/v4/palette.ts +3 -43
- package/src/modules/v4/personal.ts +1 -1
- package/src/modules/v4/placeholder.ts +6 -9
- package/src/modules/v4/qrcode.ts +108 -6
- package/src/modules/v4/username.ts +1 -1
- package/src/routes/get.ts +75 -19
- package/src/routes/post.ts +11 -3
- package/src/utils/colors.ts +91 -0
- package/src/utils/helpers.ts +90 -0
- package/src/utils/response.ts +18 -2
- package/tests/integration/api.test.ts +89 -7
- package/tests/unit/captcha.test.ts +50 -12
- package/tests/unit/chat.test.ts +4 -1
- package/tests/unit/color.test.ts +24 -4
- package/tests/unit/convert.test.ts +26 -1
- package/tests/unit/hash.test.ts +17 -10
- package/tests/unit/hyperplanning.test.ts +19 -0
- package/tests/unit/qrcode.test.ts +72 -4
- package/tests/unit/tic_tac_toe.test.ts +4 -1
- 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: (
|
|
8
|
-
kelvin: (
|
|
9
|
+
fahrenheit: (v) => (v * 9) / 5 + 32,
|
|
10
|
+
kelvin: (v) => v + 273.15,
|
|
9
11
|
},
|
|
10
12
|
fahrenheit: {
|
|
11
|
-
celsius: (
|
|
12
|
-
kelvin: (
|
|
13
|
+
celsius: (v) => ((v - 32) * 5) / 9,
|
|
14
|
+
kelvin: (v) => ((v - 32) * 5) / 9 + 273.15,
|
|
13
15
|
},
|
|
14
16
|
kelvin: {
|
|
15
|
-
celsius: (
|
|
16
|
-
fahrenheit: (
|
|
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:
|
|
101
|
+
value: number,
|
|
22
102
|
from: string,
|
|
23
103
|
to: string,
|
|
24
104
|
): { from: string; to: string; value: number; result: number } {
|
|
25
|
-
const
|
|
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(
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|
package/src/modules/v4/domain.ts
CHANGED
package/src/modules/v4/hash.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { getHashes, createHash } from 'crypto';
|
|
2
2
|
|
|
3
|
-
export
|
|
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))
|
|
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)
|
|
8
|
-
|
|
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 '
|
|
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
|
-
|
|
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:
|
|
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,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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
|
92
|
-
const
|
|
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;
|
package/src/modules/v4/qrcode.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
}
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/src/routes/post.ts
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
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
|
}
|