@20syldev/api 4.5.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 (71) hide show
  1. package/README.md +9 -4
  2. package/eslint.config.js +4 -0
  3. package/package.json +3 -2
  4. package/src/app.ts +7 -6
  5. package/src/config/env.ts +1 -0
  6. package/src/config/versions.ts +7 -4
  7. package/src/middleware/error.ts +2 -1
  8. package/src/middleware/json.ts +1 -1
  9. package/src/middleware/logger.ts +2 -1
  10. package/src/middleware/ratelimit.ts +4 -3
  11. package/src/middleware/version.ts +2 -1
  12. package/src/modules/v3.js +1 -1
  13. package/src/modules/v4/agent.ts +104 -0
  14. package/src/modules/v4/algorithms.ts +79 -0
  15. package/src/modules/v4/captcha.ts +8 -0
  16. package/src/modules/v4/chat.ts +10 -2
  17. package/src/modules/v4/color.ts +8 -1
  18. package/src/modules/v4/convert.ts +9 -0
  19. package/src/modules/v4/dice.ts +7 -0
  20. package/src/modules/v4/domain.ts +6 -1
  21. package/src/modules/v4/encode.ts +71 -0
  22. package/src/modules/v4/geo.ts +10 -0
  23. package/src/modules/v4/hash.ts +10 -1
  24. package/src/modules/v4/hyperplanning.ts +10 -1
  25. package/src/modules/v4/ip.ts +134 -0
  26. package/src/modules/v4/levenshtein.ts +8 -0
  27. package/src/modules/v4/palette.ts +9 -1
  28. package/src/modules/v4/personal.ts +5 -0
  29. package/src/modules/v4/placeholder.ts +8 -0
  30. package/src/modules/v4/qrcode.ts +9 -1
  31. package/src/modules/v4/statistics.ts +7 -0
  32. package/src/modules/v4/text.ts +30 -0
  33. package/src/modules/v4/tic_tac_toe.ts +11 -2
  34. package/src/modules/v4/time.ts +11 -0
  35. package/src/modules/v4/token.ts +10 -1
  36. package/src/modules/v4/username.ts +5 -0
  37. package/src/modules/v4/validate.ts +21 -0
  38. package/src/modules/v4.ts +3 -1
  39. package/src/routes/delete.ts +2 -1
  40. package/src/routes/get.ts +57 -7
  41. package/src/routes/index.ts +4 -3
  42. package/src/routes/patch.ts +2 -1
  43. package/src/routes/post.ts +5 -4
  44. package/src/storage/index.ts +1 -1
  45. package/src/utils/response.ts +1 -0
  46. package/tests/integration/api.test.ts +87 -2
  47. package/tests/unit/agent.test.ts +113 -0
  48. package/tests/unit/algorithms.test.ts +2 -1
  49. package/tests/unit/captcha.test.ts +2 -1
  50. package/tests/unit/chat.test.ts +2 -1
  51. package/tests/unit/color.test.ts +2 -1
  52. package/tests/unit/convert.test.ts +2 -1
  53. package/tests/unit/dice.test.ts +2 -1
  54. package/tests/unit/domain.test.ts +2 -1
  55. package/tests/unit/encode.test.ts +8 -7
  56. package/tests/unit/geo.test.ts +2 -1
  57. package/tests/unit/hash.test.ts +2 -1
  58. package/tests/unit/hyperplanning.test.ts +2 -1
  59. package/tests/unit/ip.test.ts +140 -0
  60. package/tests/unit/levenshtein.test.ts +2 -1
  61. package/tests/unit/palette.test.ts +2 -1
  62. package/tests/unit/personal.test.ts +2 -1
  63. package/tests/unit/placeholder.test.ts +2 -1
  64. package/tests/unit/qrcode.test.ts +2 -1
  65. package/tests/unit/statistics.test.ts +2 -1
  66. package/tests/unit/text.test.ts +3 -2
  67. package/tests/unit/tic_tac_toe.test.ts +2 -1
  68. package/tests/unit/time.test.ts +2 -1
  69. package/tests/unit/token.test.ts +2 -1
  70. package/tests/unit/username.test.ts +2 -1
  71. package/tests/unit/validate.test.ts +3 -2
package/src/routes/get.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { Router, type Request, type Response } from 'express';
2
- import { versions } from '../config/versions.js';
1
+ import { type Request, type Response, Router } from 'express';
2
+
3
3
  import { env } from '../config/env.js';
4
- import { ipLimits } from '../storage/index.js';
5
- import { chatStorage } from '../storage/index.js';
4
+ import { versions } from '../config/versions.js';
6
5
  import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
7
- import { error } from '../utils/response.js';
8
- import { since } from '../utils/helpers.js';
9
- import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
6
+ import type { UserAgentResult } from '../modules/v4/agent.js';
10
7
  import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
11
8
  import type { ColorResult } from '../modules/v4/color.js';
9
+ import type { IpResult } from '../modules/v4/ip.js';
10
+ import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
11
+ import { chatStorage, ipLimits } from '../storage/index.js';
12
+ import { since } from '../utils/helpers.js';
13
+ import { error } from '../utils/response.js';
12
14
 
13
15
  const router = Router();
14
16
 
@@ -174,6 +176,54 @@ router.get('/:version/convert', (req: Request, res: Response) => {
174
176
  }
175
177
  });
176
178
 
179
+ // Echo request headers
180
+ router.get('/:version/headers', (req: Request, res: Response) => {
181
+ const redacted = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
182
+ let headers: Record<string, unknown> = {};
183
+
184
+ for (const [k, v] of Object.entries(req.headers)) {
185
+ headers[k] = redacted.has(k) ? '[redacted]' : v;
186
+ }
187
+
188
+ const filter = req.query.filter as string | undefined;
189
+ if (filter) {
190
+ const keys = new Set(filter.split(',').map((k) => k.trim().toLowerCase()));
191
+ headers = Object.fromEntries(Object.entries(headers).filter(([k]) => keys.has(k)));
192
+ }
193
+
194
+ res.jsonResponse({
195
+ count: Object.keys(headers).length,
196
+ headers,
197
+ ip: req.ip,
198
+ method: req.method,
199
+ url: req.originalUrl,
200
+ });
201
+ });
202
+
203
+ // Analyze an IP address
204
+ router.get('/:version/ip', (req: Request, res: Response) => {
205
+ const address = (req.query.address as string | undefined) ?? req.ip ?? '';
206
+ try {
207
+ const ipFn = (req.module as Record<string, unknown>).ip as (a: string) => IpResult;
208
+ const result = ipFn(address);
209
+ res.jsonResponse(result);
210
+ } catch (err) {
211
+ error(res, 400, (err as Error).message, `${req.version}/ip`);
212
+ }
213
+ });
214
+
215
+ // Parse a User-Agent string
216
+ router.get('/:version/agent', (req: Request, res: Response) => {
217
+ const ua = (req.query.ua as string | undefined) ?? (req.headers['user-agent'] as string) ?? '';
218
+ try {
219
+ const agentFn = (req.module as Record<string, unknown>).agent as (ua: string) => UserAgentResult;
220
+ const result = agentFn(ua);
221
+ res.jsonResponse(result);
222
+ } catch (err) {
223
+ error(res, 400, (err as Error).message, `${req.version}/agent`);
224
+ }
225
+ });
226
+
177
227
  // Generate domain informations
178
228
  router.get('/:version/domain', (req: Request, res: Response) => {
179
229
  try {
@@ -1,8 +1,9 @@
1
- import { Router, type Request, type Response } from 'express';
1
+ import { type Request, type Response, Router } from 'express';
2
+
2
3
  import { versions } from '../config/versions.js';
3
- import { ipLimits } from '../storage/index.js';
4
- import { logger } from '../middleware/logger.js';
5
4
  import { APP_VERSION, DOCS_URL, START_TIME } from '../constants.js';
5
+ import { logger } from '../middleware/logger.js';
6
+ import { ipLimits } from '../storage/index.js';
6
7
 
7
8
  const router = Router();
8
9
 
@@ -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
  /**
@@ -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;
@@ -558,6 +559,90 @@ describe('Payload size limit', () => {
558
559
  });
559
560
  });
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
+
561
646
  describe('Prototype access on dynamic endpoints', () => {
562
647
  test('algorithms?method=toString returns 400', async () => {
563
648
  const { status } = await getJson('/v4/algorithms?method=toString');
@@ -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;
@@ -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', () => {