@20syldev/api 4.5.0 → 4.7.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 +11 -4
- package/eslint.config.js +4 -0
- package/package.json +3 -2
- package/src/app.ts +7 -6
- package/src/config/env.ts +1 -0
- package/src/config/versions.ts +10 -2
- package/src/middleware/error.ts +2 -1
- package/src/middleware/json.ts +1 -1
- package/src/middleware/logger.ts +2 -1
- package/src/middleware/ratelimit.ts +4 -3
- package/src/middleware/version.ts +2 -1
- package/src/modules/v3.js +1 -1
- package/src/modules/v4/address.ts +413 -0
- package/src/modules/v4/agent.ts +104 -0
- package/src/modules/v4/algorithms.ts +79 -0
- package/src/modules/v4/captcha.ts +8 -0
- package/src/modules/v4/chat.ts +10 -2
- package/src/modules/v4/color.ts +8 -1
- package/src/modules/v4/convert.ts +9 -0
- package/src/modules/v4/dice.ts +7 -0
- package/src/modules/v4/domain.ts +6 -1
- package/src/modules/v4/encode.ts +71 -0
- package/src/modules/v4/geo.ts +10 -0
- package/src/modules/v4/hash.ts +10 -1
- package/src/modules/v4/hyperplanning.ts +10 -1
- package/src/modules/v4/ip.ts +134 -0
- package/src/modules/v4/levenshtein.ts +8 -0
- package/src/modules/v4/palette.ts +9 -1
- package/src/modules/v4/password.ts +381 -0
- package/src/modules/v4/personal.ts +9 -16
- package/src/modules/v4/placeholder.ts +8 -0
- package/src/modules/v4/qrcode.ts +9 -1
- package/src/modules/v4/statistics.ts +7 -0
- package/src/modules/v4/text.ts +30 -0
- package/src/modules/v4/tic_tac_toe.ts +11 -2
- package/src/modules/v4/time.ts +11 -0
- package/src/modules/v4/token.ts +18 -8
- package/src/modules/v4/username.ts +5 -0
- package/src/modules/v4/validate.ts +21 -0
- package/src/modules/v4.ts +5 -1
- package/src/routes/delete.ts +2 -1
- package/src/routes/get.ts +115 -7
- package/src/routes/index.ts +4 -3
- package/src/routes/patch.ts +2 -1
- package/src/routes/post.ts +5 -4
- package/src/storage/index.ts +1 -1
- package/src/utils/response.ts +1 -0
- package/tests/integration/api.test.ts +142 -2
- package/tests/unit/address.test.ts +84 -0
- package/tests/unit/agent.test.ts +113 -0
- package/tests/unit/algorithms.test.ts +2 -1
- package/tests/unit/captcha.test.ts +2 -1
- package/tests/unit/chat.test.ts +2 -1
- package/tests/unit/color.test.ts +2 -1
- package/tests/unit/convert.test.ts +2 -1
- package/tests/unit/dice.test.ts +2 -1
- package/tests/unit/domain.test.ts +2 -1
- package/tests/unit/encode.test.ts +8 -7
- package/tests/unit/geo.test.ts +2 -1
- package/tests/unit/hash.test.ts +2 -1
- package/tests/unit/hyperplanning.test.ts +2 -1
- package/tests/unit/ip.test.ts +140 -0
- package/tests/unit/levenshtein.test.ts +2 -1
- package/tests/unit/palette.test.ts +2 -1
- package/tests/unit/password.test.ts +86 -0
- package/tests/unit/personal.test.ts +2 -1
- package/tests/unit/placeholder.test.ts +2 -1
- package/tests/unit/qrcode.test.ts +2 -1
- package/tests/unit/statistics.test.ts +2 -1
- package/tests/unit/text.test.ts +3 -2
- package/tests/unit/tic_tac_toe.test.ts +2 -1
- package/tests/unit/time.test.ts +2 -1
- package/tests/unit/token.test.ts +2 -1
- package/tests/unit/username.test.ts +2 -1
- package/tests/unit/validate.test.ts +3 -2
package/src/modules/v4/text.ts
CHANGED
|
@@ -142,6 +142,13 @@ export interface TextStats {
|
|
|
142
142
|
mostFrequentChar: string;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Analyzes a text string and returns character, word, sentence, and reading time statistics.
|
|
147
|
+
*
|
|
148
|
+
* @param value - The text to analyze
|
|
149
|
+
* @returns Object containing character counts, word/sentence/paragraph counts, reading time, and most frequent character
|
|
150
|
+
* @throws Error if value is missing, not a string, or too long
|
|
151
|
+
*/
|
|
145
152
|
export function stats(value: string): TextStats {
|
|
146
153
|
checkText(value);
|
|
147
154
|
|
|
@@ -168,6 +175,13 @@ export function stats(value: string): TextStats {
|
|
|
168
175
|
return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, mostFrequentChar };
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Converts a string to a URL-friendly slug.
|
|
180
|
+
*
|
|
181
|
+
* @param value - The string to slugify
|
|
182
|
+
* @returns Lowercase hyphenated slug with diacritics and special characters removed
|
|
183
|
+
* @throws Error if value is missing, not a string, or too long
|
|
184
|
+
*/
|
|
171
185
|
export function slug(value: string): string {
|
|
172
186
|
checkText(value);
|
|
173
187
|
return value
|
|
@@ -179,6 +193,14 @@ export function slug(value: string): string {
|
|
|
179
193
|
.replace(/[\s-]+/g, '-');
|
|
180
194
|
}
|
|
181
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Generates Lorem Ipsum placeholder text.
|
|
198
|
+
*
|
|
199
|
+
* @param type - Content type: "words", "sentences", or "paragraphs"
|
|
200
|
+
* @param count - Number of words, sentences, or paragraphs to generate
|
|
201
|
+
* @returns Generated Lorem Ipsum text
|
|
202
|
+
* @throws Error if count is out of range or type is not one of the accepted values
|
|
203
|
+
*/
|
|
182
204
|
export function lorem(type: string, count: string): string {
|
|
183
205
|
const n = Number(count) || 5;
|
|
184
206
|
if (n < 1 || n > 500) throw new Error('Count must be between 1 and 500');
|
|
@@ -283,6 +305,14 @@ function numberToEnglish(n: number): string {
|
|
|
283
305
|
throw new Error('Number must be less than 1 billion');
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Converts an integer to its written-out word form in French or English.
|
|
310
|
+
*
|
|
311
|
+
* @param value - The integer to convert (must be less than 1 billion)
|
|
312
|
+
* @param lang - Language: "fr" for French or "en" for English
|
|
313
|
+
* @returns The number written out in words
|
|
314
|
+
* @throws Error if value is not an integer, exceeds the maximum, or lang is not supported
|
|
315
|
+
*/
|
|
286
316
|
export function number(value: string, lang: string): string {
|
|
287
317
|
const n = Number(value);
|
|
288
318
|
if (isNaN(n)) throw new Error('Value must be a number');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto';
|
|
2
|
-
|
|
3
|
-
import
|
|
2
|
+
|
|
3
|
+
import { GAME_CLEANUP_TTL, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, SESSION_TTL } from '../../constants.js';
|
|
4
|
+
import type { TicTacToeGame, TicTacToeMove, TicTacToeStorage } from '../../types/storage.js';
|
|
4
5
|
|
|
5
6
|
interface TicTacToeParams {
|
|
6
7
|
username?: string;
|
|
@@ -17,6 +18,14 @@ interface GameResult {
|
|
|
17
18
|
tie?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Handles Tic-Tac-Toe game actions including playing a move, fetching game state, listing games, and forfeiting.
|
|
23
|
+
*
|
|
24
|
+
* @param action - The action to perform: "play", "fetch", "list", or "forfeit"
|
|
25
|
+
* @param params - Game parameters including username, move, session, game ID, and shared storage
|
|
26
|
+
* @returns Game state or action result depending on the action
|
|
27
|
+
* @throws Error if a required parameter is missing, the action is invalid, or a rate limit is exceeded
|
|
28
|
+
*/
|
|
20
29
|
export default function tic_tac_toe(action: string, params: TicTacToeParams): Record<string, unknown> {
|
|
21
30
|
const storage = params.storage;
|
|
22
31
|
|
package/src/modules/v4/time.ts
CHANGED
|
@@ -24,6 +24,17 @@ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo',
|
|
|
24
24
|
type TimeFormat = (typeof validFormats)[number];
|
|
25
25
|
type Timezone = (typeof validTimezones)[number];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Returns the current or a random date/time in various formats and timezones.
|
|
29
|
+
*
|
|
30
|
+
* @param type - "live" for the current time or "random" for a random date within a range
|
|
31
|
+
* @param start - Optional start date for random mode (YYYY-MM-DD)
|
|
32
|
+
* @param end - Optional end date for random mode (YYYY-MM-DD)
|
|
33
|
+
* @param format - Optional specific format to return (e.g. "iso", "timestamp", "year")
|
|
34
|
+
* @param timezone - Optional timezone (e.g. "UTC", "Europe/Paris")
|
|
35
|
+
* @returns Object containing all time formats, or a single format if specified
|
|
36
|
+
* @throws Error if type, format, or timezone is invalid
|
|
37
|
+
*/
|
|
27
38
|
export default function time(
|
|
28
39
|
type: string = 'live',
|
|
29
40
|
start?: string,
|
package/src/modules/v4/token.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { randomBytes } from 'crypto';
|
|
1
|
+
import { randomBytes, randomInt } from 'crypto';
|
|
2
2
|
import { v4 } from 'uuid';
|
|
3
|
-
import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../../constants.js';
|
|
4
3
|
|
|
4
|
+
import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../../constants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a cryptographically random token of the specified length and type.
|
|
8
|
+
*
|
|
9
|
+
* @param len - Token length (must be between 12 and 4096)
|
|
10
|
+
* @param type - Character set to use: "alpha", "alphanum", "base64", "hex", "num", "punct", "urlsafe", or "uuid"
|
|
11
|
+
* @returns The generated token string
|
|
12
|
+
* @throws Error if length is out of range or the type is not valid
|
|
13
|
+
*/
|
|
5
14
|
export default function token(len: number, type: string = 'alphanum'): string {
|
|
6
15
|
if (isNaN(len) || len < MIN_TOKEN_LENGTH) {
|
|
7
16
|
throw new Error('Length must be a number greater than or equal to 12');
|
|
@@ -11,11 +20,8 @@ export default function token(len: number, type: string = 'alphanum'): string {
|
|
|
11
20
|
throw new Error('Length cannot exceed 4096');
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
const genToken = (chars: string, length: number): string =>
|
|
15
|
-
|
|
16
|
-
return chars[Math.floor(Math.random() * chars.length)];
|
|
17
|
-
}).join('');
|
|
18
|
-
};
|
|
23
|
+
const genToken = (chars: string, length: number): string =>
|
|
24
|
+
Array.from({ length }, () => chars[randomInt(chars.length)]).join('');
|
|
19
25
|
|
|
20
26
|
const tokenTypes: Record<string, () => string> = {
|
|
21
27
|
alpha: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
|
|
@@ -31,7 +37,11 @@ export default function token(len: number, type: string = 'alphanum'): string {
|
|
|
31
37
|
num: () => genToken('0123456789', len),
|
|
32
38
|
punct: () => genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
|
|
33
39
|
urlsafe: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
|
|
34
|
-
uuid: () =>
|
|
40
|
+
uuid: () => {
|
|
41
|
+
let result = '';
|
|
42
|
+
while (result.length < len) result += v4().replace(/-/g, '');
|
|
43
|
+
return result.slice(0, len);
|
|
44
|
+
},
|
|
35
45
|
};
|
|
36
46
|
|
|
37
47
|
const lowerType = type.toLowerCase();
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { random, randomNumber } from '../../utils/helpers.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Generates a random username combining adjectives, animals, and job titles.
|
|
5
|
+
*
|
|
6
|
+
* @returns Object containing the generated username, the number used, and the components (adjective, animal, job)
|
|
7
|
+
*/
|
|
3
8
|
export default function username(): Record<string, unknown> {
|
|
4
9
|
const adj = [
|
|
5
10
|
'Happy',
|
|
@@ -7,6 +7,13 @@ function checkValue(value: string): void {
|
|
|
7
7
|
throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Validates a credit/debit card number using the Luhn algorithm.
|
|
12
|
+
*
|
|
13
|
+
* @param value - Card number string (digits, spaces, or dashes allowed)
|
|
14
|
+
* @returns Object containing the validity result and the sanitized digit string
|
|
15
|
+
* @throws Error if value is missing, contains non-digit characters, or has an invalid length
|
|
16
|
+
*/
|
|
10
17
|
export function luhn(value: string): { valid: boolean; value: string } {
|
|
11
18
|
checkValue(value);
|
|
12
19
|
const digits = value.replace(/\s|-/g, '');
|
|
@@ -28,6 +35,13 @@ export function luhn(value: string): { valid: boolean; value: string } {
|
|
|
28
35
|
return { valid: sum % 10 === 0, value: digits };
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Validates an IBAN using the mod-97 checksum algorithm.
|
|
40
|
+
*
|
|
41
|
+
* @param value - IBAN string (spaces allowed)
|
|
42
|
+
* @returns Object containing the validity result, sanitized IBAN, and country code
|
|
43
|
+
* @throws Error if value is missing, has an invalid format, or is out of length bounds
|
|
44
|
+
*/
|
|
31
45
|
export function iban(value: string): { valid: boolean; value: string; country?: string } {
|
|
32
46
|
checkValue(value);
|
|
33
47
|
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
@@ -48,6 +62,13 @@ export function iban(value: string): { valid: boolean; value: string; country?:
|
|
|
48
62
|
return { valid: remainder === 1, value: cleaned, country: cleaned.slice(0, 2) };
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Validates an email address against a basic format check.
|
|
67
|
+
*
|
|
68
|
+
* @param value - The email address string to validate
|
|
69
|
+
* @returns Object containing the validity result and the original value
|
|
70
|
+
* @throws Error if value is missing, not a string, or too long
|
|
71
|
+
*/
|
|
51
72
|
export function email(value: string): { valid: boolean; value: string } {
|
|
52
73
|
checkValue(value);
|
|
53
74
|
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
package/src/modules/v4.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export { default as address } from './v4/address.js';
|
|
2
|
+
export { default as agent } from './v4/agent.js';
|
|
1
3
|
export * as algorithms from './v4/algorithms.js';
|
|
2
|
-
export { default as chat } from './v4/chat.js';
|
|
3
4
|
export { default as captcha } from './v4/captcha.js';
|
|
5
|
+
export { default as chat } from './v4/chat.js';
|
|
4
6
|
export { default as color } from './v4/color.js';
|
|
5
7
|
export { default as convert } from './v4/convert.js';
|
|
6
8
|
export { default as dice } from './v4/dice.js';
|
|
@@ -9,8 +11,10 @@ export * as encode from './v4/encode.js';
|
|
|
9
11
|
export { default as geo } from './v4/geo.js';
|
|
10
12
|
export { default as hash } from './v4/hash.js';
|
|
11
13
|
export { default as hyperplanning } from './v4/hyperplanning.js';
|
|
14
|
+
export { default as ip } from './v4/ip.js';
|
|
12
15
|
export { default as levenshtein } from './v4/levenshtein.js';
|
|
13
16
|
export { default as palette } from './v4/palette.js';
|
|
17
|
+
export { default as password } from './v4/password.js';
|
|
14
18
|
export { default as personal } from './v4/personal.js';
|
|
15
19
|
export { default as placeholder } from './v4/placeholder.js';
|
|
16
20
|
export { default as qrcode } from './v4/qrcode.js';
|
package/src/routes/delete.ts
CHANGED
package/src/routes/get.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { type Request, type Response, Router } from 'express';
|
|
2
|
+
|
|
3
3
|
import { env } from '../config/env.js';
|
|
4
|
-
import {
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
|
|
6
|
+
import type { AddressResult } from '../modules/v4/address.js';
|
|
7
|
+
import type { UserAgentResult } from '../modules/v4/agent.js';
|
|
10
8
|
import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
|
|
11
9
|
import type { ColorResult } from '../modules/v4/color.js';
|
|
10
|
+
import type { IpResult } from '../modules/v4/ip.js';
|
|
11
|
+
import type { PasswordResult } from '../modules/v4/password.js';
|
|
12
|
+
import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
|
|
13
|
+
import { chatStorage, ipLimits } from '../storage/index.js';
|
|
14
|
+
import { since } from '../utils/helpers.js';
|
|
15
|
+
import { error } from '../utils/response.js';
|
|
12
16
|
|
|
13
17
|
const router = Router();
|
|
14
18
|
|
|
@@ -174,6 +178,75 @@ router.get('/:version/convert', (req: Request, res: Response) => {
|
|
|
174
178
|
}
|
|
175
179
|
});
|
|
176
180
|
|
|
181
|
+
// Generate a fictional postal address
|
|
182
|
+
router.get('/:version/address', (req: Request, res: Response) => {
|
|
183
|
+
const { country, count } = req.query;
|
|
184
|
+
const { version } = req.params;
|
|
185
|
+
|
|
186
|
+
const addressFn = (req.module as { address?: (c?: string, n?: number) => AddressResult }).address;
|
|
187
|
+
if (!addressFn) {
|
|
188
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/address`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsedCount = count !== undefined ? parseInt(count as string, 10) : 1;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = addressFn(country as string | undefined, parsedCount);
|
|
196
|
+
res.jsonResponse(result);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
error(res, 400, (err as Error).message, `${req.version}/address`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Echo request headers
|
|
203
|
+
router.get('/:version/headers', (req: Request, res: Response) => {
|
|
204
|
+
const redacted = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
|
|
205
|
+
let headers: Record<string, unknown> = {};
|
|
206
|
+
|
|
207
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
208
|
+
headers[k] = redacted.has(k) ? '[redacted]' : v;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const filter = req.query.filter as string | undefined;
|
|
212
|
+
if (filter) {
|
|
213
|
+
const keys = new Set(filter.split(',').map((k) => k.trim().toLowerCase()));
|
|
214
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([k]) => keys.has(k)));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
res.jsonResponse({
|
|
218
|
+
count: Object.keys(headers).length,
|
|
219
|
+
headers,
|
|
220
|
+
ip: req.ip,
|
|
221
|
+
method: req.method,
|
|
222
|
+
url: req.originalUrl,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Analyze an IP address
|
|
227
|
+
router.get('/:version/ip', (req: Request, res: Response) => {
|
|
228
|
+
const address = (req.query.address as string | undefined) ?? req.ip ?? '';
|
|
229
|
+
try {
|
|
230
|
+
const ipFn = (req.module as Record<string, unknown>).ip as (a: string) => IpResult;
|
|
231
|
+
const result = ipFn(address);
|
|
232
|
+
res.jsonResponse(result);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
error(res, 400, (err as Error).message, `${req.version}/ip`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Parse a User-Agent string
|
|
239
|
+
router.get('/:version/agent', (req: Request, res: Response) => {
|
|
240
|
+
const ua = (req.query.ua as string | undefined) ?? (req.headers['user-agent'] as string) ?? '';
|
|
241
|
+
try {
|
|
242
|
+
const agentFn = (req.module as Record<string, unknown>).agent as (ua: string) => UserAgentResult;
|
|
243
|
+
const result = agentFn(ua);
|
|
244
|
+
res.jsonResponse(result);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
error(res, 400, (err as Error).message, `${req.version}/agent`);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
177
250
|
// Generate domain informations
|
|
178
251
|
router.get('/:version/domain', (req: Request, res: Response) => {
|
|
179
252
|
try {
|
|
@@ -326,6 +399,41 @@ router.get('/:version/palette', (req: Request, res: Response) => {
|
|
|
326
399
|
}
|
|
327
400
|
});
|
|
328
401
|
|
|
402
|
+
// Generate a password or passphrase
|
|
403
|
+
router.get('/:version/password', (req: Request, res: Response) => {
|
|
404
|
+
const { type, length, uppercase, lowercase, digits, symbols, exclude, count, separator } = req.query;
|
|
405
|
+
const { version } = req.params;
|
|
406
|
+
|
|
407
|
+
const passwordFn = (
|
|
408
|
+
req.module as { password?: (t: string, l: number, o: Record<string, unknown>) => PasswordResult }
|
|
409
|
+
).password;
|
|
410
|
+
if (!passwordFn) {
|
|
411
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/password`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const parseBool = (v: unknown, def: boolean): boolean => (v === undefined ? def : v !== 'false');
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const result = passwordFn(
|
|
419
|
+
(type as string) ?? 'random',
|
|
420
|
+
length !== undefined ? parseInt(length as string, 10) : 16,
|
|
421
|
+
{
|
|
422
|
+
uppercase: parseBool(uppercase, true),
|
|
423
|
+
lowercase: parseBool(lowercase, true),
|
|
424
|
+
digits: parseBool(digits, true),
|
|
425
|
+
symbols: parseBool(symbols, false),
|
|
426
|
+
exclude: (exclude as string) ?? '',
|
|
427
|
+
count: count !== undefined ? parseInt(count as string, 10) : 1,
|
|
428
|
+
separator: (separator as string) ?? '-',
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
res.jsonResponse(result);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
error(res, 400, (err as Error).message, `${req.version}/password`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
329
437
|
// Generate personal data
|
|
330
438
|
router.get('/:version/personal', (req: Request, res: Response) => {
|
|
331
439
|
try {
|
package/src/routes/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
|
package/src/routes/patch.ts
CHANGED
package/src/routes/post.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
+
import { error } from '../utils/response.js';
|
|
7
8
|
|
|
8
9
|
const router = Router();
|
|
9
10
|
|
package/src/storage/index.ts
CHANGED
package/src/utils/response.ts
CHANGED
|
@@ -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,145 @@ 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
|
+
|
|
646
|
+
describe('GET /v4/address', () => {
|
|
647
|
+
test('returns one address by default', async () => {
|
|
648
|
+
const { status, body } = await getJson('/v4/address');
|
|
649
|
+
assert.equal(status, 200);
|
|
650
|
+
assert.equal((body.addresses as unknown[]).length, 1);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('country=fr returns a French address', async () => {
|
|
654
|
+
const { body } = await getJson('/v4/address?country=fr');
|
|
655
|
+
const a = (body.addresses as Record<string, string>[])[0]!;
|
|
656
|
+
assert.equal(a.countryCode, 'FR');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('count=3 returns 3 addresses', async () => {
|
|
660
|
+
const { body } = await getJson('/v4/address?count=3');
|
|
661
|
+
assert.equal((body.addresses as unknown[]).length, 3);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('unknown country returns 400', async () => {
|
|
665
|
+
const { status } = await getJson('/v4/address?country=xx');
|
|
666
|
+
assert.equal(status, 400);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
describe('GET /v4/password', () => {
|
|
671
|
+
test('returns one password by default', async () => {
|
|
672
|
+
const { status, body } = await getJson('/v4/password');
|
|
673
|
+
assert.equal(status, 200);
|
|
674
|
+
assert.equal((body.passwords as string[]).length, 1);
|
|
675
|
+
assert.equal((body.passwords as string[])[0]!.length, 16);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('length param is respected', async () => {
|
|
679
|
+
const { body } = await getJson('/v4/password?length=32');
|
|
680
|
+
assert.equal((body.passwords as string[])[0]!.length, 32);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('passphrase mode returns words', async () => {
|
|
684
|
+
const { body } = await getJson('/v4/password?type=passphrase&length=4');
|
|
685
|
+
assert.equal(body.type, 'passphrase');
|
|
686
|
+
assert.equal((body.passwords as string[])[0]!.split('-').length, 4);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('count=5 returns 5 passwords', async () => {
|
|
690
|
+
const { body } = await getJson('/v4/password?count=5');
|
|
691
|
+
assert.equal((body.passwords as string[]).length, 5);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('no charset active returns 400', async () => {
|
|
695
|
+
const { status } = await getJson('/v4/password?uppercase=false&lowercase=false&digits=false&symbols=false');
|
|
696
|
+
assert.equal(status, 400);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
|
|
561
701
|
describe('Prototype access on dynamic endpoints', () => {
|
|
562
702
|
test('algorithms?method=toString returns 400', async () => {
|
|
563
703
|
const { status } = await getJson('/v4/algorithms?method=toString');
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import address from '../../src/modules/v4/address.js';
|
|
5
|
+
|
|
6
|
+
describe('address', () => {
|
|
7
|
+
describe('default behavior', () => {
|
|
8
|
+
test('returns an addresses array with one entry by default', () => {
|
|
9
|
+
const r = address();
|
|
10
|
+
assert.equal(r.addresses.length, 1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('each address has required fields', () => {
|
|
14
|
+
const r = address();
|
|
15
|
+
const a = r.addresses[0]!;
|
|
16
|
+
assert.ok(a.street);
|
|
17
|
+
assert.ok(a.city);
|
|
18
|
+
assert.ok(a.zip);
|
|
19
|
+
assert.ok(a.state);
|
|
20
|
+
assert.ok(a.country);
|
|
21
|
+
assert.ok(a.countryCode);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('country filtering', () => {
|
|
26
|
+
test('fr returns a French address', () => {
|
|
27
|
+
const r = address('fr');
|
|
28
|
+
assert.equal(r.addresses[0]!.countryCode, 'FR');
|
|
29
|
+
assert.equal(r.addresses[0]!.country, 'France');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('us returns a US address', () => {
|
|
33
|
+
const r = address('us');
|
|
34
|
+
assert.equal(r.addresses[0]!.countryCode, 'US');
|
|
35
|
+
assert.equal(r.addresses[0]!.country, 'United States');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('uk returns a UK address', () => {
|
|
39
|
+
const r = address('uk');
|
|
40
|
+
assert.equal(r.addresses[0]!.countryCode, 'UK');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('de returns a German address', () => {
|
|
44
|
+
const r = address('de');
|
|
45
|
+
assert.equal(r.addresses[0]!.countryCode, 'DE');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('es returns a Spanish address', () => {
|
|
49
|
+
const r = address('es');
|
|
50
|
+
assert.equal(r.addresses[0]!.countryCode, 'ES');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('country code is case-insensitive', () => {
|
|
54
|
+
const r = address('FR');
|
|
55
|
+
assert.equal(r.addresses[0]!.countryCode, 'FR');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('count', () => {
|
|
60
|
+
test('count=5 returns 5 addresses', () => {
|
|
61
|
+
const r = address('fr', 5);
|
|
62
|
+
assert.equal(r.addresses.length, 5);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('count=10 returns 10 addresses', () => {
|
|
66
|
+
const r = address('us', 10);
|
|
67
|
+
assert.equal(r.addresses.length, 10);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('errors', () => {
|
|
72
|
+
test('unknown country throws', () => {
|
|
73
|
+
assert.throws(() => address('xx'), /Unknown country code/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('count=0 throws', () => {
|
|
77
|
+
assert.throws(() => address('fr', 0), /Count must be between/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('count=11 throws', () => {
|
|
81
|
+
assert.throws(() => address('fr', 11), /Count must be between/);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|