@20syldev/api 4.4.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +9 -4
  2. package/dist/config/env.js +5 -1
  3. package/dist/config/env.js.map +1 -1
  4. package/dist/config/plans.js +44 -0
  5. package/dist/config/plans.js.map +1 -0
  6. package/dist/constants.js +1 -0
  7. package/dist/constants.js.map +1 -1
  8. package/dist/middleware/cors.js +7 -2
  9. package/dist/middleware/cors.js.map +1 -1
  10. package/dist/middleware/error.js +3 -1
  11. package/dist/middleware/error.js.map +1 -1
  12. package/dist/middleware/ratelimit.js +39 -21
  13. package/dist/middleware/ratelimit.js.map +1 -1
  14. package/dist/modules/v3/chat.js +1 -1
  15. package/dist/modules/v3/chat.js.map +1 -1
  16. package/dist/modules/v3/domain.js +1 -1
  17. package/dist/modules/v3/domain.js.map +1 -1
  18. package/dist/modules/v3/hyperplanning.js +1 -1
  19. package/dist/modules/v3/hyperplanning.js.map +1 -1
  20. package/dist/modules/v3/personal.js +1 -1
  21. package/dist/modules/v3/personal.js.map +1 -1
  22. package/dist/modules/v3/username.js +1 -1
  23. package/dist/modules/v3/username.js.map +1 -1
  24. package/dist/modules/v4/convert.js.map +1 -1
  25. package/dist/modules/v4/hyperplanning.js +31 -1
  26. package/dist/modules/v4/hyperplanning.js.map +1 -1
  27. package/dist/routes/get.js +9 -8
  28. package/dist/routes/get.js.map +1 -1
  29. package/dist/routes/post.js +2 -1
  30. package/dist/routes/post.js.map +1 -1
  31. package/dist/utils/colors.js +59 -20
  32. package/dist/utils/colors.js.map +1 -1
  33. package/dist/utils/helpers.js +67 -16
  34. package/dist/utils/helpers.js.map +1 -1
  35. package/dist/utils/response.js +18 -2
  36. package/dist/utils/response.js.map +1 -1
  37. package/eslint.config.js +4 -0
  38. package/package.json +3 -2
  39. package/src/app.ts +7 -6
  40. package/src/config/env.ts +7 -1
  41. package/src/config/plans.ts +61 -0
  42. package/src/config/versions.ts +7 -4
  43. package/src/constants.ts +1 -0
  44. package/src/middleware/cors.ts +8 -2
  45. package/src/middleware/error.ts +12 -3
  46. package/src/middleware/json.ts +1 -1
  47. package/src/middleware/logger.ts +2 -1
  48. package/src/middleware/ratelimit.ts +44 -21
  49. package/src/middleware/version.ts +2 -1
  50. package/src/modules/v3.js +1 -1
  51. package/src/modules/v4/agent.ts +104 -0
  52. package/src/modules/v4/algorithms.ts +79 -0
  53. package/src/modules/v4/captcha.ts +8 -0
  54. package/src/modules/v4/chat.ts +10 -2
  55. package/src/modules/v4/color.ts +8 -1
  56. package/src/modules/v4/convert.ts +9 -0
  57. package/src/modules/v4/dice.ts +7 -0
  58. package/src/modules/v4/domain.ts +6 -1
  59. package/src/modules/v4/encode.ts +71 -0
  60. package/src/modules/v4/geo.ts +10 -0
  61. package/src/modules/v4/hash.ts +10 -1
  62. package/src/modules/v4/hyperplanning.ts +37 -2
  63. package/src/modules/v4/ip.ts +134 -0
  64. package/src/modules/v4/levenshtein.ts +8 -0
  65. package/src/modules/v4/palette.ts +9 -1
  66. package/src/modules/v4/personal.ts +5 -0
  67. package/src/modules/v4/placeholder.ts +8 -0
  68. package/src/modules/v4/qrcode.ts +9 -1
  69. package/src/modules/v4/statistics.ts +7 -0
  70. package/src/modules/v4/text.ts +30 -0
  71. package/src/modules/v4/tic_tac_toe.ts +11 -2
  72. package/src/modules/v4/time.ts +11 -0
  73. package/src/modules/v4/token.ts +10 -1
  74. package/src/modules/v4/username.ts +5 -0
  75. package/src/modules/v4/validate.ts +21 -0
  76. package/src/modules/v4.ts +3 -1
  77. package/src/routes/delete.ts +2 -1
  78. package/src/routes/get.ts +61 -11
  79. package/src/routes/index.ts +4 -3
  80. package/src/routes/patch.ts +2 -1
  81. package/src/routes/post.ts +5 -4
  82. package/src/storage/index.ts +1 -1
  83. package/src/utils/response.ts +11 -2
  84. package/tests/integration/api.test.ts +125 -2
  85. package/tests/unit/agent.test.ts +113 -0
  86. package/tests/unit/algorithms.test.ts +2 -1
  87. package/tests/unit/captcha.test.ts +2 -1
  88. package/tests/unit/chat.test.ts +2 -1
  89. package/tests/unit/color.test.ts +2 -1
  90. package/tests/unit/convert.test.ts +2 -1
  91. package/tests/unit/dice.test.ts +2 -1
  92. package/tests/unit/domain.test.ts +2 -1
  93. package/tests/unit/encode.test.ts +8 -7
  94. package/tests/unit/geo.test.ts +2 -1
  95. package/tests/unit/hash.test.ts +2 -1
  96. package/tests/unit/hyperplanning.test.ts +21 -1
  97. package/tests/unit/ip.test.ts +140 -0
  98. package/tests/unit/levenshtein.test.ts +2 -1
  99. package/tests/unit/palette.test.ts +2 -1
  100. package/tests/unit/personal.test.ts +2 -1
  101. package/tests/unit/placeholder.test.ts +2 -1
  102. package/tests/unit/qrcode.test.ts +2 -1
  103. package/tests/unit/statistics.test.ts +2 -1
  104. package/tests/unit/text.test.ts +3 -2
  105. package/tests/unit/tic_tac_toe.test.ts +2 -1
  106. package/tests/unit/time.test.ts +2 -1
  107. package/tests/unit/token.test.ts +2 -1
  108. package/tests/unit/username.test.ts +2 -1
  109. package/tests/unit/validate.test.ts +3 -2
@@ -1,4 +1,5 @@
1
- import { Router, type Request, type Response } from 'express';
1
+ import { type Request, type Response, Router } from 'express';
2
+
2
3
  import { ticTacToeStorage } from '../storage/index.js';
3
4
  import { error } from '../utils/response.js';
4
5
 
@@ -1,9 +1,10 @@
1
- import { Router, type Request, type Response } from 'express';
1
+ import { type Request, type Response, Router } from 'express';
2
+
3
+ import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../constants.js';
4
+ import type { HashResult } from '../modules/v4/hash.js';
2
5
  import { chatStorage, ticTacToeStorage } from '../storage/index.js';
3
- import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../constants.js';
4
- import { error } from '../utils/response.js';
5
6
  import { since } from '../utils/helpers.js';
6
- import type { HashResult } from '../modules/v4/hash.js';
7
+ import { error } from '../utils/response.js';
7
8
 
8
9
  const router = Router();
9
10
 
@@ -1,4 +1,4 @@
1
- import type { ChatStorage, TicTacToeStorage, IpLimits } from '../types/storage.js';
1
+ import type { ChatStorage, IpLimits, TicTacToeStorage } from '../types/storage.js';
2
2
 
3
3
  export const ipLimits: IpLimits = {};
4
4
 
@@ -1,4 +1,5 @@
1
1
  import type { Response } from 'express';
2
+
2
3
  import { DOCS_URL, STATUS_MESSAGES } from '../constants.js';
3
4
 
4
5
  /**
@@ -10,10 +11,18 @@ import { DOCS_URL, STATUS_MESSAGES } from '../constants.js';
10
11
  * @param docPath - Optional documentation path appended to the base URL
11
12
  */
12
13
  export function error(res: Response, status: number, message: string, docPath?: string): void {
13
- res.status(status).jsonResponse({
14
+ const body = {
14
15
  message: STATUS_MESSAGES[status] ?? 'Error',
15
16
  error: message,
16
17
  documentation: docPath ? `${DOCS_URL}/${docPath}` : DOCS_URL,
17
18
  status: String(status),
18
- });
19
+ };
20
+
21
+ if (typeof res.jsonResponse === 'function') {
22
+ res.status(status).jsonResponse(body);
23
+ } else {
24
+ res.status(status)
25
+ .setHeader('Content-Type', 'application/json')
26
+ .send(JSON.stringify(body, null, 2));
27
+ }
19
28
  }
@@ -1,7 +1,8 @@
1
- import { test, describe, before, after } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
3
- import type { AddressInfo } from 'node:net';
4
2
  import type { Server } from 'node:http';
3
+ import type { AddressInfo } from 'node:net';
4
+ import { after, before, describe, test } from 'node:test';
5
+
5
6
  import app from '../../src/app.js';
6
7
 
7
8
  let server: Server;
@@ -536,3 +537,125 @@ describe('POST /v4/token', () => {
536
537
  assert.equal(status, 400);
537
538
  });
538
539
  });
540
+
541
+ // --- Security ---
542
+
543
+ describe('Security headers', () => {
544
+ test('returns X-Content-Type-Options and X-Frame-Options', async () => {
545
+ const res = await fetch(`${baseUrl}/v4/color`);
546
+ assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
547
+ assert.equal(res.headers.get('x-frame-options'), 'DENY');
548
+ });
549
+ });
550
+
551
+ describe('Payload size limit', () => {
552
+ test('body > 10kb returns 413', async () => {
553
+ const res = await fetch(`${baseUrl}/v4/hash`, {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ text: 'A'.repeat(20000), method: 'sha256' }),
557
+ });
558
+ assert.equal(res.status, 413);
559
+ });
560
+ });
561
+
562
+ describe('GET /v4/headers', () => {
563
+ test('returns headers object with count, ip, method, url', async () => {
564
+ const { status, body } = await getJson('/v4/headers');
565
+ assert.equal(status, 200);
566
+ assert.ok(typeof body.count === 'number');
567
+ assert.ok(typeof body.headers === 'object');
568
+ assert.ok('method' in body);
569
+ assert.equal(body.method, 'GET');
570
+ assert.ok((body.url as string).includes('/v4/headers'));
571
+ });
572
+
573
+ test('filter param returns only matching headers', async () => {
574
+ const { body } = await getJson('/v4/headers?filter=host');
575
+ const headers = body.headers as Record<string, unknown>;
576
+ assert.ok('host' in headers);
577
+ assert.equal(body.count, 1);
578
+ });
579
+
580
+ test('cookie header is redacted', async () => {
581
+ const res = await fetch(`${baseUrl}/v4/headers`, {
582
+ headers: { Cookie: 'session=secret123' },
583
+ });
584
+ const body = (await res.json()) as Record<string, unknown>;
585
+ const headers = body.headers as Record<string, unknown>;
586
+ assert.equal(headers['cookie'], '[redacted]');
587
+ });
588
+ });
589
+
590
+ describe('GET /v4/ip', () => {
591
+ test('returns valid ip result with no address param', async () => {
592
+ const { status, body } = await getJson('/v4/ip');
593
+ assert.equal(status, 200);
594
+ assert.ok(['IPv4', 'IPv6'].includes(body.version as string));
595
+ assert.ok(typeof body.binary === 'string');
596
+ assert.ok(typeof body.reverse === 'string');
597
+ });
598
+
599
+ test('loopback 127.0.0.1', async () => {
600
+ const { status, body } = await getJson('/v4/ip?address=127.0.0.1');
601
+ assert.equal(status, 200);
602
+ assert.equal(body.type, 'loopback');
603
+ assert.equal(body.version, 'IPv4');
604
+ });
605
+
606
+ test('private 192.168.1.1', async () => {
607
+ const { body } = await getJson('/v4/ip?address=192.168.1.1');
608
+ assert.equal(body.type, 'private');
609
+ assert.equal(body.decimal, 3232235777);
610
+ });
611
+
612
+ test('invalid address returns 400', async () => {
613
+ const { status } = await getJson('/v4/ip?address=notanip');
614
+ assert.equal(status, 400);
615
+ });
616
+ });
617
+
618
+ describe('GET /v4/agent', () => {
619
+ test('returns parsed result with browser, os, device, engine, bot', async () => {
620
+ const { status, body } = await getJson('/v4/agent');
621
+ assert.equal(status, 200);
622
+ assert.ok(typeof body.browser === 'object');
623
+ assert.ok(typeof body.os === 'object');
624
+ assert.ok(typeof body.device === 'object');
625
+ assert.ok(typeof body.engine === 'object');
626
+ assert.ok(typeof body.bot === 'boolean');
627
+ });
628
+
629
+ test('custom ua param is parsed', async () => {
630
+ const ua = encodeURIComponent(
631
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
632
+ );
633
+ const { body } = await getJson(`/v4/agent?ua=${ua}`);
634
+ const browser = body.browser as Record<string, string>;
635
+ assert.equal(browser.name, 'Firefox');
636
+ assert.equal(browser.major, '121');
637
+ });
638
+
639
+ test('bot ua detected', async () => {
640
+ const ua = encodeURIComponent('Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)');
641
+ const { body } = await getJson(`/v4/agent?ua=${ua}`);
642
+ assert.equal(body.bot, true);
643
+ });
644
+ });
645
+
646
+ describe('Prototype access on dynamic endpoints', () => {
647
+ test('algorithms?method=toString returns 400', async () => {
648
+ const { status } = await getJson('/v4/algorithms?method=toString');
649
+ assert.equal(status, 400);
650
+ });
651
+
652
+ test('encode?method=constructor returns 400', async () => {
653
+ const { status } = await getJson('/v4/encode?method=constructor&text=hello');
654
+ assert.equal(status, 400);
655
+ });
656
+
657
+ test('validate?type=hasOwnProperty returns 400', async () => {
658
+ const { status } = await getJson('/v4/validate?type=hasOwnProperty&value=test');
659
+ assert.equal(status, 400);
660
+ });
661
+ });
@@ -0,0 +1,113 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
4
+ import agent from '../../src/modules/v4/agent.js';
5
+
6
+ const CHROME_MAC =
7
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
8
+ const FIREFOX_WIN = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0';
9
+ const SAFARI_IPHONE =
10
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
11
+ const EDGE_WIN =
12
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0';
13
+ const GOOGLEBOT = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
14
+ const IPAD =
15
+ 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
16
+
17
+ describe('agent', () => {
18
+ describe('browser detection', () => {
19
+ test('Chrome on macOS', () => {
20
+ const r = agent(CHROME_MAC);
21
+ assert.equal(r.browser.name, 'Chrome');
22
+ assert.equal(r.browser.major, '120');
23
+ });
24
+
25
+ test('Firefox on Windows', () => {
26
+ const r = agent(FIREFOX_WIN);
27
+ assert.equal(r.browser.name, 'Firefox');
28
+ assert.equal(r.browser.major, '121');
29
+ });
30
+
31
+ test('Safari on iPhone', () => {
32
+ const r = agent(SAFARI_IPHONE);
33
+ assert.equal(r.browser.name, 'Safari');
34
+ });
35
+
36
+ test('Edge detected before Chrome (both present in UA)', () => {
37
+ const r = agent(EDGE_WIN);
38
+ assert.equal(r.browser.name, 'Edge');
39
+ });
40
+
41
+ test('unknown browser returns unknown', () => {
42
+ const r = agent('UnknownBrowser/1.0');
43
+ assert.equal(r.browser.name, 'unknown');
44
+ });
45
+ });
46
+
47
+ describe('OS detection', () => {
48
+ test('macOS from Chrome UA', () => {
49
+ const r = agent(CHROME_MAC);
50
+ assert.equal(r.os.name, 'macOS');
51
+ assert.equal(r.os.version, '10.15.7');
52
+ });
53
+
54
+ test('Windows 10/11 from Firefox UA', () => {
55
+ const r = agent(FIREFOX_WIN);
56
+ assert.equal(r.os.name, 'Windows 10/11');
57
+ });
58
+
59
+ test('iOS from iPhone UA', () => {
60
+ const r = agent(SAFARI_IPHONE);
61
+ assert.equal(r.os.name, 'iOS');
62
+ assert.equal(r.os.version, '17.0');
63
+ });
64
+ });
65
+
66
+ describe('device detection', () => {
67
+ test('desktop from Chrome macOS', () => {
68
+ assert.equal(agent(CHROME_MAC).device.type, 'desktop');
69
+ });
70
+
71
+ test('mobile from iPhone UA', () => {
72
+ assert.equal(agent(SAFARI_IPHONE).device.type, 'mobile');
73
+ });
74
+
75
+ test('tablet from iPad UA', () => {
76
+ assert.equal(agent(IPAD).device.type, 'tablet');
77
+ });
78
+
79
+ test('Apple vendor from iPhone', () => {
80
+ assert.equal(agent(SAFARI_IPHONE).device.vendor, 'Apple');
81
+ });
82
+ });
83
+
84
+ describe('engine detection', () => {
85
+ test('Chrome uses Blink', () => {
86
+ assert.equal(agent(CHROME_MAC).engine.name, 'Blink');
87
+ });
88
+
89
+ test('Firefox uses Gecko', () => {
90
+ assert.equal(agent(FIREFOX_WIN).engine.name, 'Gecko');
91
+ });
92
+
93
+ test('Edge uses Blink', () => {
94
+ assert.equal(agent(EDGE_WIN).engine.name, 'Blink');
95
+ });
96
+ });
97
+
98
+ describe('bot detection', () => {
99
+ test('Googlebot is a bot', () => {
100
+ assert.equal(agent(GOOGLEBOT).bot, true);
101
+ });
102
+
103
+ test('Chrome is not a bot', () => {
104
+ assert.equal(agent(CHROME_MAC).bot, false);
105
+ });
106
+ });
107
+
108
+ describe('errors', () => {
109
+ test('throws on empty string', () => {
110
+ assert.throws(() => agent(''), /required/);
111
+ });
112
+ });
113
+ });
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import {
4
5
  anagram,
5
6
  factorial,
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import captcha from '../../src/modules/v4/captcha.js';
4
5
 
5
6
  const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import chat from '../../src/modules/v4/chat.js';
4
5
  import type { ChatStorage } from '../../src/types/storage.js';
5
6
 
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import color from '../../src/modules/v4/color.js';
4
5
 
5
6
  describe('color', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import convert from '../../src/modules/v4/convert.js';
4
5
 
5
6
  describe('convert', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import dice from '../../src/modules/v4/dice.js';
4
5
 
5
6
  describe('dice', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import domain from '../../src/modules/v4/domain.js';
4
5
 
5
6
  describe('domain', () => {
@@ -1,16 +1,17 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import {
4
- base64encode,
5
5
  base64decode,
6
- urlencode,
7
- urldecode,
6
+ base64encode,
7
+ binary,
8
+ caesar,
8
9
  morse,
9
- unmorse,
10
10
  rot13,
11
- caesar,
12
- binary,
13
11
  unbinary,
12
+ unmorse,
13
+ urldecode,
14
+ urlencode,
14
15
  } from '../../src/modules/v4/encode.js';
15
16
 
16
17
  describe('encode', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import geo from '../../src/modules/v4/geo.js';
4
5
 
5
6
  describe('geo', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import hash from '../../src/modules/v4/hash.js';
4
5
 
5
6
  describe('hash', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe, before, after } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { after, before, describe, test } from 'node:test';
3
+
3
4
  import hyperplanning from '../../src/modules/v4/hyperplanning.js';
4
5
 
5
6
  const ORIGINAL_FETCH = globalThis.fetch;
@@ -70,6 +71,25 @@ describe('hyperplanning', () => {
70
71
  const events = await hyperplanning('https://fake.test/cal.ics');
71
72
  assert.equal(events.length, 0);
72
73
  });
74
+
75
+ test('throws on HTTP URL', async () => {
76
+ await assert.rejects(() => hyperplanning('http://fake.test/cal.ics'), /Only HTTPS/);
77
+ });
78
+
79
+ test('throws on private IP', async () => {
80
+ await assert.rejects(() => hyperplanning('https://127.0.0.1/cal.ics'), /private/);
81
+ await assert.rejects(() => hyperplanning('https://192.168.1.1/cal.ics'), /private/);
82
+ await assert.rejects(() => hyperplanning('https://10.0.0.1/cal.ics'), /private/);
83
+ await assert.rejects(() => hyperplanning('https://169.254.169.254/latest'), /private/);
84
+ });
85
+
86
+ test('throws on localhost', async () => {
87
+ await assert.rejects(() => hyperplanning('https://localhost/cal.ics'), /private/);
88
+ });
89
+
90
+ test('throws on invalid URL', async () => {
91
+ await assert.rejects(() => hyperplanning('not-a-url'), /Invalid URL/);
92
+ });
73
93
  });
74
94
 
75
95
  before(() => {
@@ -0,0 +1,140 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
4
+ import ip from '../../src/modules/v4/ip.js';
5
+
6
+ describe('ip', () => {
7
+ describe('IPv4 — type detection', () => {
8
+ test('public IP returns public type and class A', () => {
9
+ const result = ip('8.8.8.8');
10
+ assert.equal(result.version, 'IPv4');
11
+ assert.equal(result.type, 'public');
12
+ assert.equal(result.class, 'A');
13
+ });
14
+
15
+ test('private 10.x.x.x', () => {
16
+ const result = ip('10.0.0.1');
17
+ assert.equal(result.type, 'private');
18
+ assert.equal(result.range, '10.0.0.0/8');
19
+ });
20
+
21
+ test('private 172.16-31.x.x', () => {
22
+ const result = ip('172.16.0.1');
23
+ assert.equal(result.type, 'private');
24
+ assert.equal(result.range, '172.16.0.0/12');
25
+ });
26
+
27
+ test('private 192.168.x.x', () => {
28
+ const result = ip('192.168.1.1');
29
+ assert.equal(result.type, 'private');
30
+ assert.equal(result.class, 'C');
31
+ assert.equal(result.range, '192.168.0.0/16');
32
+ });
33
+
34
+ test('loopback 127.0.0.1', () => {
35
+ const result = ip('127.0.0.1');
36
+ assert.equal(result.type, 'loopback');
37
+ assert.equal(result.range, '127.0.0.0/8');
38
+ });
39
+
40
+ test('link-local 169.254.x.x', () => {
41
+ const result = ip('169.254.1.1');
42
+ assert.equal(result.type, 'link-local');
43
+ });
44
+
45
+ test('multicast 224.x.x.x', () => {
46
+ const result = ip('224.0.0.1');
47
+ assert.equal(result.type, 'multicast');
48
+ });
49
+
50
+ test('broadcast 255.255.255.255', () => {
51
+ const result = ip('255.255.255.255');
52
+ assert.equal(result.type, 'broadcast');
53
+ assert.equal(result.class, 'E');
54
+ });
55
+ });
56
+
57
+ describe('IPv4 — representations', () => {
58
+ test('binary of 192.168.1.1', () => {
59
+ const result = ip('192.168.1.1');
60
+ assert.equal(result.binary, '11000000.10101000.00000001.00000001');
61
+ });
62
+
63
+ test('decimal of 192.168.1.1', () => {
64
+ const result = ip('192.168.1.1');
65
+ assert.equal(result.decimal, 3232235777);
66
+ });
67
+
68
+ test('decimal of 8.8.8.8', () => {
69
+ const result = ip('8.8.8.8');
70
+ assert.equal(result.decimal, 134744072);
71
+ });
72
+
73
+ test('reverse DNS of 192.168.1.1', () => {
74
+ const result = ip('192.168.1.1');
75
+ assert.equal(result.reverse, '1.1.168.192.in-addr.arpa');
76
+ });
77
+
78
+ test('class B for 128.x', () => {
79
+ assert.equal(ip('128.0.0.1').class, 'B');
80
+ });
81
+
82
+ test('class C for 192.x (non-private)', () => {
83
+ assert.equal(ip('200.0.0.1').class, 'C');
84
+ });
85
+ });
86
+
87
+ describe('IPv6', () => {
88
+ test('loopback ::1', () => {
89
+ const result = ip('::1');
90
+ assert.equal(result.version, 'IPv6');
91
+ assert.equal(result.type, 'loopback');
92
+ assert.equal(result.range, '::1/128');
93
+ });
94
+
95
+ test('link-local fe80::1', () => {
96
+ const result = ip('fe80::1');
97
+ assert.equal(result.type, 'link-local');
98
+ assert.equal(result.range, 'fe80::/10');
99
+ });
100
+
101
+ test('unique-local fc00::1', () => {
102
+ const result = ip('fc00::1');
103
+ assert.equal(result.type, 'private');
104
+ assert.equal(result.range, 'fc00::/7');
105
+ });
106
+
107
+ test('multicast ff02::1', () => {
108
+ const result = ip('ff02::1');
109
+ assert.equal(result.type, 'multicast');
110
+ });
111
+
112
+ test(':: expansion produces 8 groups', () => {
113
+ const result = ip('::1');
114
+ assert.equal(result.ip.split(':').length, 8);
115
+ });
116
+
117
+ test('reverse DNS ends in .ip6.arpa', () => {
118
+ const result = ip('::1');
119
+ assert.ok(result.reverse.endsWith('.ip6.arpa'));
120
+ });
121
+ });
122
+
123
+ describe('errors', () => {
124
+ test('throws on empty string', () => {
125
+ assert.throws(() => ip(''), /required/);
126
+ });
127
+
128
+ test('throws on invalid IPv4 octet > 255', () => {
129
+ assert.throws(() => ip('256.0.0.1'), /Invalid IPv4/);
130
+ });
131
+
132
+ test('throws on incomplete IPv4', () => {
133
+ assert.throws(() => ip('192.168.1'), /Invalid IPv4/);
134
+ });
135
+
136
+ test('throws on plain string', () => {
137
+ assert.throws(() => ip('notanip'), /Invalid IP address format/);
138
+ });
139
+ });
140
+ });
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import levenshtein from '../../src/modules/v4/levenshtein.js';
4
5
 
5
6
  describe('levenshtein', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import palette from '../../src/modules/v4/palette.js';
4
5
 
5
6
  describe('palette', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import personal from '../../src/modules/v4/personal.js';
4
5
 
5
6
  describe('personal', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import placeholder from '../../src/modules/v4/placeholder.js';
4
5
 
5
6
  describe('placeholder', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import qrcode from '../../src/modules/v4/qrcode.js';
4
5
 
5
6
  const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import statistics from '../../src/modules/v4/statistics.js';
4
5
 
5
6
  describe('statistics', () => {
@@ -1,6 +1,7 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
3
- import { slug, stats, lorem, number } from '../../src/modules/v4/text.js';
2
+ import { describe, test } from 'node:test';
3
+
4
+ import { lorem, number, slug, stats } from '../../src/modules/v4/text.js';
4
5
 
5
6
  describe('text', () => {
6
7
  describe('slug', () => {
@@ -1,5 +1,6 @@
1
- import { test, describe } from 'node:test';
2
1
  import { strict as assert } from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+
3
4
  import tic_tac_toe from '../../src/modules/v4/tic_tac_toe.js';
4
5
  import type { TicTacToeStorage } from '../../src/types/storage.js';
5
6