@20syldev/api 4.2.0 → 4.4.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 (98) hide show
  1. package/README.md +3 -3
  2. package/dist/app.js +4 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/config/versions.js +19 -0
  5. package/dist/config/versions.js.map +1 -1
  6. package/dist/constants.js +2 -2
  7. package/dist/constants.js.map +1 -1
  8. package/dist/modules/v3/chat.js +1 -1
  9. package/dist/modules/v3/chat.js.map +1 -1
  10. package/dist/modules/v3/domain.js +1 -1
  11. package/dist/modules/v3/domain.js.map +1 -1
  12. package/dist/modules/v3/hyperplanning.js +1 -1
  13. package/dist/modules/v3/hyperplanning.js.map +1 -1
  14. package/dist/modules/v3/personal.js +1 -1
  15. package/dist/modules/v3/personal.js.map +1 -1
  16. package/dist/modules/v3/username.js +1 -1
  17. package/dist/modules/v3/username.js.map +1 -1
  18. package/dist/modules/v4/captcha.js +60 -23
  19. package/dist/modules/v4/captcha.js.map +1 -1
  20. package/dist/modules/v4/chat.js +1 -1
  21. package/dist/modules/v4/chat.js.map +1 -1
  22. package/dist/modules/v4/color.js +44 -35
  23. package/dist/modules/v4/color.js.map +1 -1
  24. package/dist/modules/v4/colors.js +54 -0
  25. package/dist/modules/v4/colors.js.map +1 -0
  26. package/dist/modules/v4/convert.js +93 -19
  27. package/dist/modules/v4/convert.js.map +1 -1
  28. package/dist/modules/v4/domain.js +1 -1
  29. package/dist/modules/v4/domain.js.map +1 -1
  30. package/dist/modules/v4/hash.js +11 -4
  31. package/dist/modules/v4/hash.js.map +1 -1
  32. package/dist/modules/v4/hyperplanning.js +1 -1
  33. package/dist/modules/v4/hyperplanning.js.map +1 -1
  34. package/dist/modules/v4/palette.js +2 -43
  35. package/dist/modules/v4/palette.js.map +1 -1
  36. package/dist/modules/v4/personal.js +1 -1
  37. package/dist/modules/v4/personal.js.map +1 -1
  38. package/dist/modules/v4/placeholder.js +108 -52
  39. package/dist/modules/v4/placeholder.js.map +1 -1
  40. package/dist/modules/v4/qrcode.js +79 -6
  41. package/dist/modules/v4/qrcode.js.map +1 -1
  42. package/dist/modules/v4/username.js +1 -1
  43. package/dist/modules/v4/username.js.map +1 -1
  44. package/dist/modules/v4.js +6 -0
  45. package/dist/modules/v4.js.map +1 -1
  46. package/dist/routes/delete.js +16 -17
  47. package/dist/routes/delete.js.map +1 -1
  48. package/dist/routes/get.js +212 -13
  49. package/dist/routes/get.js.map +1 -1
  50. package/dist/routes/index.js +2 -2
  51. package/dist/routes/index.js.map +1 -1
  52. package/dist/routes/patch.js +2 -3
  53. package/dist/routes/patch.js.map +1 -1
  54. package/dist/routes/post.js +10 -3
  55. package/dist/routes/post.js.map +1 -1
  56. package/dist/utils/colors.js +54 -0
  57. package/dist/utils/colors.js.map +1 -0
  58. package/dist/utils/helpers.js +26 -0
  59. package/dist/utils/helpers.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/app.ts +4 -0
  62. package/src/config/versions.ts +16 -0
  63. package/src/constants.ts +0 -2
  64. package/src/middleware/cors.ts +1 -1
  65. package/src/modules/v4/captcha.ts +84 -27
  66. package/src/modules/v4/chat.ts +25 -2
  67. package/src/modules/v4/color.ts +56 -52
  68. package/src/modules/v4/convert.ts +100 -20
  69. package/src/modules/v4/domain.ts +1 -1
  70. package/src/modules/v4/geo.ts +53 -0
  71. package/src/modules/v4/hash.ts +18 -4
  72. package/src/modules/v4/hyperplanning.ts +1 -1
  73. package/src/modules/v4/palette.ts +63 -0
  74. package/src/modules/v4/personal.ts +1 -1
  75. package/src/modules/v4/placeholder.ts +189 -0
  76. package/src/modules/v4/qrcode.ts +108 -6
  77. package/src/modules/v4/tic_tac_toe.ts +33 -1
  78. package/src/modules/v4/username.ts +1 -1
  79. package/src/modules/v4.ts +3 -0
  80. package/src/routes/delete.ts +71 -0
  81. package/src/routes/get.ts +147 -15
  82. package/src/routes/patch.ts +44 -0
  83. package/src/routes/post.ts +11 -3
  84. package/src/utils/colors.ts +91 -0
  85. package/src/utils/helpers.ts +90 -0
  86. package/src/utils/response.ts +8 -0
  87. package/tests/integration/api.test.ts +113 -25
  88. package/tests/unit/captcha.test.ts +61 -0
  89. package/tests/unit/chat.test.ts +53 -0
  90. package/tests/unit/color.test.ts +24 -4
  91. package/tests/unit/convert.test.ts +26 -1
  92. package/tests/unit/geo.test.ts +45 -0
  93. package/tests/unit/hash.test.ts +17 -10
  94. package/tests/unit/palette.test.ts +53 -0
  95. package/tests/unit/placeholder.test.ts +57 -0
  96. package/tests/unit/qrcode.test.ts +82 -0
  97. package/tests/unit/tic_tac_toe.test.ts +55 -0
  98. package/src/modules/v4/utils.ts +0 -38
@@ -0,0 +1,82 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import qrcode from '../../src/modules/v4/qrcode.js';
4
+
5
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
6
+
7
+ describe('qrcode', () => {
8
+ test('returns PNG buffer by default', async () => {
9
+ const result = await qrcode({ url: 'https://example.com' });
10
+ assert.equal(result.contentType, 'image/png');
11
+ assert.ok(Buffer.isBuffer(result.body));
12
+ assert.ok((result.body as Buffer).subarray(0, 4).equals(PNG_MAGIC));
13
+ });
14
+
15
+ test('returns data URL with format=base64', async () => {
16
+ const result = await qrcode({ url: 'https://example.com', format: 'base64' });
17
+ assert.equal(result.contentType, 'application/json');
18
+ assert.match(result.body as string, /^data:image\/png;base64,/);
19
+ });
20
+
21
+ test('throws on invalid format', async () => {
22
+ await assert.rejects(() => qrcode({ url: 'https://example.com', format: 'jpg' as 'png' }), /format/);
23
+ });
24
+
25
+ test('throws on missing url', async () => {
26
+ await assert.rejects(() => qrcode({ url: '' }));
27
+ });
28
+
29
+ test('accepts custom size', async () => {
30
+ const result = await qrcode({ url: 'https://example.com', size: 400 });
31
+ assert.equal(result.contentType, 'image/png');
32
+ });
33
+
34
+ test('throws on invalid size', async () => {
35
+ await assert.rejects(() => qrcode({ url: 'https://example.com', size: 9999 }), /size/);
36
+ });
37
+
38
+ test('accepts custom colors', async () => {
39
+ const result = await qrcode({ url: 'https://example.com', dark: 'ff0000', light: 'ffffff' });
40
+ assert.equal(result.contentType, 'image/png');
41
+ });
42
+
43
+ test('throws on invalid dark color', async () => {
44
+ await assert.rejects(() => qrcode({ url: 'https://example.com', dark: 'xyz' }), /color/i);
45
+ });
46
+
47
+ test('throws on invalid light color', async () => {
48
+ await assert.rejects(() => qrcode({ url: 'https://example.com', light: 'nope' }), /color/i);
49
+ });
50
+
51
+ test('accepts correction level', async () => {
52
+ const result = await qrcode({ url: 'https://example.com', correction: 'H' });
53
+ assert.equal(result.contentType, 'image/png');
54
+ });
55
+
56
+ test('throws on invalid correction level', async () => {
57
+ await assert.rejects(() => qrcode({ url: 'https://example.com', correction: 'X' as 'L' }), /correction/);
58
+ });
59
+
60
+ test('accepts custom margin', async () => {
61
+ const result = await qrcode({ url: 'https://example.com', margin: 0 });
62
+ assert.equal(result.contentType, 'image/png');
63
+ });
64
+
65
+ test('throws on invalid margin', async () => {
66
+ await assert.rejects(() => qrcode({ url: 'https://example.com', margin: 99 }), /margin/);
67
+ });
68
+
69
+ test('throws on non-HTTPS icon URL', async () => {
70
+ await assert.rejects(
71
+ () => qrcode({ url: 'https://example.com', icon: 'http://example.com/icon.png' }),
72
+ /HTTPS/,
73
+ );
74
+ });
75
+
76
+ test('throws on unreachable icon URL', async () => {
77
+ await assert.rejects(
78
+ () => qrcode({ url: 'https://example.com', icon: 'https://localhost:1/nope.png' }),
79
+ /Failed to fetch icon|fetch/i,
80
+ );
81
+ });
82
+ });
@@ -0,0 +1,55 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import tic_tac_toe from '../../src/modules/v4/tic_tac_toe.js';
4
+ import type { TicTacToeStorage } from '../../src/types/storage.js';
5
+
6
+ function makeStorage(): TicTacToeStorage {
7
+ return { games: {}, sessions: {}, rateLimits: {} };
8
+ }
9
+
10
+ describe('tic_tac_toe', () => {
11
+ test('list returns empty games array', () => {
12
+ const result = tic_tac_toe('list', { storage: makeStorage() });
13
+ assert.ok(Array.isArray(result.games));
14
+ });
15
+
16
+ test('play creates a game and makes a move', () => {
17
+ const storage = makeStorage();
18
+ const result = tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST1', storage });
19
+ assert.ok((result.message as string).includes('Move sent'));
20
+ });
21
+
22
+ test('fetch returns game state', () => {
23
+ const storage = makeStorage();
24
+ tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST2', storage });
25
+ const result = tic_tac_toe('fetch', { username: 'alice', game: 'TEST2', storage });
26
+ assert.equal(result.id, 'TEST2');
27
+ });
28
+
29
+ test('forfeit throws on non-player', () => {
30
+ const storage = makeStorage();
31
+ tic_tac_toe('play', { username: 'alice', move: '1-1', session: 'a1', game: 'TEST3', storage });
32
+ assert.throws(
33
+ () => tic_tac_toe('forfeit', { username: 'charlie', session: 'c1', game: 'TEST3', storage }),
34
+ /not a player/,
35
+ );
36
+ });
37
+
38
+ test('throws on missing username', () => {
39
+ assert.throws(
40
+ () => tic_tac_toe('play', { move: '1-1', session: 's', game: 'G', storage: makeStorage() }),
41
+ /username/,
42
+ );
43
+ });
44
+
45
+ test('throws on missing move', () => {
46
+ assert.throws(
47
+ () => tic_tac_toe('play', { username: 'alice', session: 's', game: 'G', storage: makeStorage() }),
48
+ /move/i,
49
+ );
50
+ });
51
+
52
+ test('throws on invalid action', () => {
53
+ assert.throws(() => tic_tac_toe('reset', { username: 'alice', storage: makeStorage() }), /Invalid action/);
54
+ });
55
+ });
@@ -1,38 +0,0 @@
1
- export function random<T>(arr: T[]): T {
2
- return arr[Math.floor(Math.random() * arr.length)]!;
3
- }
4
-
5
- export function randomNumber(min: number, max: number): number {
6
- return Math.floor(Math.random() * (max - min + 1)) + min;
7
- }
8
-
9
- export function genIP(): string {
10
- return `${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`;
11
- }
12
-
13
- export function formatDate(date: Date): string {
14
- return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
15
- }
16
-
17
- export function envList(key: string): string[] | null {
18
- const v = process.env[key];
19
- return v && v !== 'undefined' ? v.split(' ') : null;
20
- }
21
-
22
- export function checkRateLimit(
23
- rateLimits: Record<string, number[]>,
24
- userId: string,
25
- timestamp: number,
26
- window = 10000,
27
- limit = 50,
28
- ): boolean {
29
- rateLimits[userId] = (rateLimits[userId] ?? []).filter((ts) => timestamp - ts < window);
30
-
31
- if (rateLimits[userId]!.length > limit) {
32
- const remainingTime = Math.ceil((rateLimits[userId]![0]! + window - timestamp) / 1000);
33
- throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
34
- }
35
-
36
- rateLimits[userId]!.push(timestamp);
37
- return false;
38
- }