@20syldev/api 4.6.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 +5 -3
- package/package.json +1 -1
- package/src/config/versions.ts +5 -0
- package/src/modules/v4/address.ts +413 -0
- package/src/modules/v4/password.ts +381 -0
- package/src/modules/v4/personal.ts +4 -16
- package/src/modules/v4/token.ts +8 -7
- package/src/modules/v4.ts +2 -0
- package/src/routes/get.ts +58 -0
- package/tests/integration/api.test.ts +55 -0
- package/tests/unit/address.test.ts +84 -0
- package/tests/unit/password.test.ts +86 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://api.sylvain.sh"><img src="https://api.sylvain.sh/favicon.ico" alt="Logo" width="25%" height="auto"/></a>
|
|
3
3
|
|
|
4
4
|
# API Personnelle
|
|
5
|
-
[](https://github.com/20syldev/api/releases/latest)
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -32,10 +32,10 @@ Pour démarrer un serveur local avec tous les endpoints :
|
|
|
32
32
|
$ npm run build && npm start
|
|
33
33
|
```
|
|
34
34
|
```console
|
|
35
|
-
> @20syldev/api@4.
|
|
35
|
+
> @20syldev/api@4.7.0 build
|
|
36
36
|
> tsc
|
|
37
37
|
|
|
38
|
-
> @20syldev/api@4.
|
|
38
|
+
> @20syldev/api@4.7.0 start
|
|
39
39
|
> node dist/app.js
|
|
40
40
|
|
|
41
41
|
API is running on
|
|
@@ -88,6 +88,7 @@ L'API v4 expose plusieurs modules que vous pouvez importer :
|
|
|
88
88
|
|
|
89
89
|
```js
|
|
90
90
|
import {
|
|
91
|
+
address, // Génération d'adresses postales aléatoires
|
|
91
92
|
agent, // Analyse de User-Agent (navigateur, OS, appareil)
|
|
92
93
|
algorithms, // Fonctions algorithmiques diverses
|
|
93
94
|
captcha, // Génération d'images captcha
|
|
@@ -103,6 +104,7 @@ import {
|
|
|
103
104
|
ip, // Analyse d'adresses IPv4/IPv6
|
|
104
105
|
levenshtein, // Distance entre chaînes
|
|
105
106
|
palette, // Génération de palettes de couleurs
|
|
107
|
+
password, // Génération de mots de passe sécurisés
|
|
106
108
|
personal, // Informations personnelles aléatoires
|
|
107
109
|
placeholder, // Génération d'images placeholder
|
|
108
110
|
qrcode, // Génération de QR codes
|
package/package.json
CHANGED
package/src/config/versions.ts
CHANGED
|
@@ -78,6 +78,7 @@ const v3 = {
|
|
|
78
78
|
|
|
79
79
|
const v4 = {
|
|
80
80
|
get: merge(v3.get, [
|
|
81
|
+
{ name: 'address', path: '/address(&country={code}&count={n})' },
|
|
81
82
|
{ name: 'agent', path: '/agent(&ua={string})' },
|
|
82
83
|
{
|
|
83
84
|
name: 'captcha',
|
|
@@ -91,6 +92,10 @@ const v4 = {
|
|
|
91
92
|
{ name: 'headers', path: '/headers(&filter={header1,header2})' },
|
|
92
93
|
{ name: 'ip', path: '/ip(&address={ip})' },
|
|
93
94
|
{ name: 'palette', path: '/palette?color={#hex}&type={type}' },
|
|
95
|
+
{
|
|
96
|
+
name: 'password',
|
|
97
|
+
path: '/password(&type={random|passphrase}&length={n}&uppercase={bool}&lowercase={bool}&digits={bool}&symbols={bool}&exclude={chars}&count={n}&separator={char})',
|
|
98
|
+
},
|
|
94
99
|
{
|
|
95
100
|
name: 'placeholder',
|
|
96
101
|
path: '/placeholder?type={image|skeleton}&width={w}&height={h}(&bg={hex}&color={hex}&text={text}&rows={n}&avatar={bool})',
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { random, randomNumber } from '../../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const MAX_COUNT = 10;
|
|
4
|
+
|
|
5
|
+
interface CountryData {
|
|
6
|
+
name: string;
|
|
7
|
+
streetTypes: string[];
|
|
8
|
+
streetNames: string[];
|
|
9
|
+
cities: string[];
|
|
10
|
+
regions: string[];
|
|
11
|
+
zipFormat: () => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function randomChar(chars: string): string {
|
|
15
|
+
return chars[Math.floor(Math.random() * chars.length)]!;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
19
|
+
|
|
20
|
+
const countries: Record<string, CountryData> = {
|
|
21
|
+
fr: {
|
|
22
|
+
name: 'France',
|
|
23
|
+
streetTypes: ['Rue', 'Avenue', 'Boulevard', 'Allée', 'Impasse', 'Place', 'Chemin', 'Route', 'Voie', 'Passage'],
|
|
24
|
+
streetNames: [
|
|
25
|
+
'de la Paix',
|
|
26
|
+
'Victor Hugo',
|
|
27
|
+
'Jean Jaurès',
|
|
28
|
+
'de la République',
|
|
29
|
+
'du Général de Gaulle',
|
|
30
|
+
'de la Liberté',
|
|
31
|
+
'des Fleurs',
|
|
32
|
+
'du Moulin',
|
|
33
|
+
'de la Forêt',
|
|
34
|
+
'des Lilas',
|
|
35
|
+
'du Château',
|
|
36
|
+
'de la Mairie',
|
|
37
|
+
'de la Gare',
|
|
38
|
+
'du Commerce',
|
|
39
|
+
'de la Fontaine',
|
|
40
|
+
'de la Croix',
|
|
41
|
+
'des Vignes',
|
|
42
|
+
'du Marché',
|
|
43
|
+
"de l'Église",
|
|
44
|
+
'des Acacias',
|
|
45
|
+
],
|
|
46
|
+
cities: [
|
|
47
|
+
'Paris',
|
|
48
|
+
'Lyon',
|
|
49
|
+
'Marseille',
|
|
50
|
+
'Toulouse',
|
|
51
|
+
'Nice',
|
|
52
|
+
'Nantes',
|
|
53
|
+
'Strasbourg',
|
|
54
|
+
'Montpellier',
|
|
55
|
+
'Bordeaux',
|
|
56
|
+
'Lille',
|
|
57
|
+
'Rennes',
|
|
58
|
+
'Reims',
|
|
59
|
+
'Le Havre',
|
|
60
|
+
'Saint-Étienne',
|
|
61
|
+
'Toulon',
|
|
62
|
+
'Grenoble',
|
|
63
|
+
'Dijon',
|
|
64
|
+
'Angers',
|
|
65
|
+
'Nîmes',
|
|
66
|
+
'Villeurbanne',
|
|
67
|
+
],
|
|
68
|
+
regions: [
|
|
69
|
+
'Île-de-France',
|
|
70
|
+
'Auvergne-Rhône-Alpes',
|
|
71
|
+
"Provence-Alpes-Côte d'Azur",
|
|
72
|
+
'Occitanie',
|
|
73
|
+
'Hauts-de-France',
|
|
74
|
+
'Nouvelle-Aquitaine',
|
|
75
|
+
'Grand Est',
|
|
76
|
+
'Pays de la Loire',
|
|
77
|
+
'Bretagne',
|
|
78
|
+
'Normandie',
|
|
79
|
+
'Bourgogne-Franche-Comté',
|
|
80
|
+
'Centre-Val de Loire',
|
|
81
|
+
],
|
|
82
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
83
|
+
},
|
|
84
|
+
us: {
|
|
85
|
+
name: 'United States',
|
|
86
|
+
streetTypes: ['Street', 'Avenue', 'Boulevard', 'Drive', 'Road', 'Lane', 'Court', 'Way', 'Place', 'Circle'],
|
|
87
|
+
streetNames: [
|
|
88
|
+
'Main',
|
|
89
|
+
'Oak',
|
|
90
|
+
'Maple',
|
|
91
|
+
'Cedar',
|
|
92
|
+
'Pine',
|
|
93
|
+
'Elm',
|
|
94
|
+
'Washington',
|
|
95
|
+
'Lincoln',
|
|
96
|
+
'Park',
|
|
97
|
+
'Lake',
|
|
98
|
+
'Hill',
|
|
99
|
+
'River',
|
|
100
|
+
'Sunset',
|
|
101
|
+
'Forest',
|
|
102
|
+
'Meadow',
|
|
103
|
+
'Valley',
|
|
104
|
+
'Highland',
|
|
105
|
+
'Willow',
|
|
106
|
+
'Spring',
|
|
107
|
+
'Church',
|
|
108
|
+
],
|
|
109
|
+
cities: [
|
|
110
|
+
'New York',
|
|
111
|
+
'Los Angeles',
|
|
112
|
+
'Chicago',
|
|
113
|
+
'Houston',
|
|
114
|
+
'Phoenix',
|
|
115
|
+
'Philadelphia',
|
|
116
|
+
'San Antonio',
|
|
117
|
+
'San Diego',
|
|
118
|
+
'Dallas',
|
|
119
|
+
'San Jose',
|
|
120
|
+
'Austin',
|
|
121
|
+
'Jacksonville',
|
|
122
|
+
'Fort Worth',
|
|
123
|
+
'Columbus',
|
|
124
|
+
'Charlotte',
|
|
125
|
+
'Indianapolis',
|
|
126
|
+
'San Francisco',
|
|
127
|
+
'Seattle',
|
|
128
|
+
'Denver',
|
|
129
|
+
'Nashville',
|
|
130
|
+
],
|
|
131
|
+
regions: [
|
|
132
|
+
'California',
|
|
133
|
+
'Texas',
|
|
134
|
+
'Florida',
|
|
135
|
+
'New York',
|
|
136
|
+
'Pennsylvania',
|
|
137
|
+
'Illinois',
|
|
138
|
+
'Ohio',
|
|
139
|
+
'Georgia',
|
|
140
|
+
'North Carolina',
|
|
141
|
+
'Michigan',
|
|
142
|
+
'New Jersey',
|
|
143
|
+
'Virginia',
|
|
144
|
+
'Washington',
|
|
145
|
+
'Arizona',
|
|
146
|
+
'Massachusetts',
|
|
147
|
+
'Tennessee',
|
|
148
|
+
'Indiana',
|
|
149
|
+
'Colorado',
|
|
150
|
+
'Missouri',
|
|
151
|
+
'Maryland',
|
|
152
|
+
],
|
|
153
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
154
|
+
},
|
|
155
|
+
uk: {
|
|
156
|
+
name: 'United Kingdom',
|
|
157
|
+
streetTypes: ['Street', 'Road', 'Avenue', 'Lane', 'Close', 'Drive', 'Way', 'Court', 'Grove', 'Place'],
|
|
158
|
+
streetNames: [
|
|
159
|
+
'High',
|
|
160
|
+
'Church',
|
|
161
|
+
'Station',
|
|
162
|
+
'Park',
|
|
163
|
+
'Manor',
|
|
164
|
+
'Mill',
|
|
165
|
+
'Victoria',
|
|
166
|
+
'King',
|
|
167
|
+
'Queen',
|
|
168
|
+
'Albert',
|
|
169
|
+
'George',
|
|
170
|
+
'York',
|
|
171
|
+
'Oxford',
|
|
172
|
+
'Cambridge',
|
|
173
|
+
'Windsor',
|
|
174
|
+
'Richmond',
|
|
175
|
+
'Elm',
|
|
176
|
+
'Oak',
|
|
177
|
+
'Ash',
|
|
178
|
+
'Rose',
|
|
179
|
+
],
|
|
180
|
+
cities: [
|
|
181
|
+
'London',
|
|
182
|
+
'Birmingham',
|
|
183
|
+
'Manchester',
|
|
184
|
+
'Glasgow',
|
|
185
|
+
'Liverpool',
|
|
186
|
+
'Bristol',
|
|
187
|
+
'Sheffield',
|
|
188
|
+
'Leeds',
|
|
189
|
+
'Edinburgh',
|
|
190
|
+
'Leicester',
|
|
191
|
+
'Coventry',
|
|
192
|
+
'Bradford',
|
|
193
|
+
'Cardiff',
|
|
194
|
+
'Belfast',
|
|
195
|
+
'Nottingham',
|
|
196
|
+
'Kingston upon Hull',
|
|
197
|
+
'Newcastle',
|
|
198
|
+
'Stoke-on-Trent',
|
|
199
|
+
'Southampton',
|
|
200
|
+
'Derby',
|
|
201
|
+
],
|
|
202
|
+
regions: [
|
|
203
|
+
'England',
|
|
204
|
+
'Scotland',
|
|
205
|
+
'Wales',
|
|
206
|
+
'Northern Ireland',
|
|
207
|
+
'Greater London',
|
|
208
|
+
'South East',
|
|
209
|
+
'North West',
|
|
210
|
+
'East of England',
|
|
211
|
+
'West Midlands',
|
|
212
|
+
'South West',
|
|
213
|
+
'Yorkshire',
|
|
214
|
+
'East Midlands',
|
|
215
|
+
'North East',
|
|
216
|
+
],
|
|
217
|
+
zipFormat: () =>
|
|
218
|
+
`${randomChar(ALPHA)}${randomChar(ALPHA)}${randomNumber(1, 9)} ${randomNumber(1, 9)}${randomChar(ALPHA)}${randomChar(ALPHA)}`,
|
|
219
|
+
},
|
|
220
|
+
de: {
|
|
221
|
+
name: 'Germany',
|
|
222
|
+
streetTypes: ['Straße', 'Allee', 'Weg', 'Platz', 'Gasse', 'Ring', 'Damm', 'Ufer', 'Stieg', 'Pfad'],
|
|
223
|
+
streetNames: [
|
|
224
|
+
'Haupt',
|
|
225
|
+
'Bahnhof',
|
|
226
|
+
'Schul',
|
|
227
|
+
'Kirch',
|
|
228
|
+
'Garten',
|
|
229
|
+
'Berg',
|
|
230
|
+
'Wald',
|
|
231
|
+
'Wiesen',
|
|
232
|
+
'Tal',
|
|
233
|
+
'See',
|
|
234
|
+
'Bismarck',
|
|
235
|
+
'Goethe',
|
|
236
|
+
'Schiller',
|
|
237
|
+
'Kant',
|
|
238
|
+
'Hegel',
|
|
239
|
+
'Mozart',
|
|
240
|
+
'Beethoven',
|
|
241
|
+
'Bach',
|
|
242
|
+
'Brahms',
|
|
243
|
+
'Handel',
|
|
244
|
+
],
|
|
245
|
+
cities: [
|
|
246
|
+
'Berlin',
|
|
247
|
+
'Hamburg',
|
|
248
|
+
'Munich',
|
|
249
|
+
'Cologne',
|
|
250
|
+
'Frankfurt',
|
|
251
|
+
'Stuttgart',
|
|
252
|
+
'Düsseldorf',
|
|
253
|
+
'Leipzig',
|
|
254
|
+
'Dortmund',
|
|
255
|
+
'Essen',
|
|
256
|
+
'Bremen',
|
|
257
|
+
'Dresden',
|
|
258
|
+
'Hanover',
|
|
259
|
+
'Nuremberg',
|
|
260
|
+
'Duisburg',
|
|
261
|
+
'Bochum',
|
|
262
|
+
'Wuppertal',
|
|
263
|
+
'Bielefeld',
|
|
264
|
+
'Bonn',
|
|
265
|
+
'Münster',
|
|
266
|
+
],
|
|
267
|
+
regions: [
|
|
268
|
+
'Bayern',
|
|
269
|
+
'Nordrhein-Westfalen',
|
|
270
|
+
'Baden-Württemberg',
|
|
271
|
+
'Niedersachsen',
|
|
272
|
+
'Hessen',
|
|
273
|
+
'Sachsen',
|
|
274
|
+
'Rheinland-Pfalz',
|
|
275
|
+
'Berlin',
|
|
276
|
+
'Schleswig-Holstein',
|
|
277
|
+
'Brandenburg',
|
|
278
|
+
'Sachsen-Anhalt',
|
|
279
|
+
'Thüringen',
|
|
280
|
+
'Hamburg',
|
|
281
|
+
'Mecklenburg-Vorpommern',
|
|
282
|
+
'Bremen',
|
|
283
|
+
'Saarland',
|
|
284
|
+
],
|
|
285
|
+
zipFormat: () => String(randomNumber(10000, 99999)).padStart(5, '0'),
|
|
286
|
+
},
|
|
287
|
+
es: {
|
|
288
|
+
name: 'Spain',
|
|
289
|
+
streetTypes: [
|
|
290
|
+
'Calle',
|
|
291
|
+
'Avenida',
|
|
292
|
+
'Plaza',
|
|
293
|
+
'Paseo',
|
|
294
|
+
'Rambla',
|
|
295
|
+
'Carretera',
|
|
296
|
+
'Camino',
|
|
297
|
+
'Vía',
|
|
298
|
+
'Travesía',
|
|
299
|
+
'Callejón',
|
|
300
|
+
],
|
|
301
|
+
streetNames: [
|
|
302
|
+
'Mayor',
|
|
303
|
+
'Real',
|
|
304
|
+
'Nueva',
|
|
305
|
+
'del Sol',
|
|
306
|
+
'de la Luna',
|
|
307
|
+
'del Rey',
|
|
308
|
+
'de la Reina',
|
|
309
|
+
'del Mar',
|
|
310
|
+
'de la Paz',
|
|
311
|
+
'de la Victoria',
|
|
312
|
+
'Cervantes',
|
|
313
|
+
'Goya',
|
|
314
|
+
'Velázquez',
|
|
315
|
+
'Picasso',
|
|
316
|
+
'Dalí',
|
|
317
|
+
'de España',
|
|
318
|
+
'de Colón',
|
|
319
|
+
'de la Constitución',
|
|
320
|
+
'del Prado',
|
|
321
|
+
'de las Flores',
|
|
322
|
+
],
|
|
323
|
+
cities: [
|
|
324
|
+
'Madrid',
|
|
325
|
+
'Barcelona',
|
|
326
|
+
'Valencia',
|
|
327
|
+
'Seville',
|
|
328
|
+
'Zaragoza',
|
|
329
|
+
'Málaga',
|
|
330
|
+
'Murcia',
|
|
331
|
+
'Palma',
|
|
332
|
+
'Las Palmas',
|
|
333
|
+
'Bilbao',
|
|
334
|
+
'Alicante',
|
|
335
|
+
'Córdoba',
|
|
336
|
+
'Valladolid',
|
|
337
|
+
'Vigo',
|
|
338
|
+
'Gijón',
|
|
339
|
+
'Hospitalet',
|
|
340
|
+
'A Coruña',
|
|
341
|
+
'Granada',
|
|
342
|
+
'Vitoria',
|
|
343
|
+
'Elche',
|
|
344
|
+
],
|
|
345
|
+
regions: [
|
|
346
|
+
'Andalucía',
|
|
347
|
+
'Cataluña',
|
|
348
|
+
'Madrid',
|
|
349
|
+
'Comunidad Valenciana',
|
|
350
|
+
'Galicia',
|
|
351
|
+
'Castilla y León',
|
|
352
|
+
'País Vasco',
|
|
353
|
+
'Castilla-La Mancha',
|
|
354
|
+
'Canarias',
|
|
355
|
+
'Murcia',
|
|
356
|
+
'Aragón',
|
|
357
|
+
'Extremadura',
|
|
358
|
+
'Asturias',
|
|
359
|
+
'Navarra',
|
|
360
|
+
'Cantabria',
|
|
361
|
+
'La Rioja',
|
|
362
|
+
'Baleares',
|
|
363
|
+
],
|
|
364
|
+
zipFormat: () => String(randomNumber(10000, 52999)).padStart(5, '0'),
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const countryKeys = Object.keys(countries);
|
|
369
|
+
|
|
370
|
+
export interface Address {
|
|
371
|
+
street: string;
|
|
372
|
+
city: string;
|
|
373
|
+
zip: string;
|
|
374
|
+
state: string;
|
|
375
|
+
country: string;
|
|
376
|
+
countryCode: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface AddressResult {
|
|
380
|
+
addresses: Address[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generates one or more fictional postal addresses for a given country.
|
|
385
|
+
*
|
|
386
|
+
* @param countryCode - Country code: "fr", "us", "uk", "de", or "es" (random if omitted)
|
|
387
|
+
* @param count - Number of addresses to generate (1–10)
|
|
388
|
+
* @returns Array of addresses with street, city, zip, state, country, and countryCode
|
|
389
|
+
* @throws Error if the country code is invalid or count is out of range
|
|
390
|
+
*/
|
|
391
|
+
export default function address(countryCode?: string, count: number = 1): AddressResult {
|
|
392
|
+
if (count < 1 || count > MAX_COUNT) {
|
|
393
|
+
throw new Error(`Count must be between 1 and ${MAX_COUNT}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const code = (countryCode ?? random(countryKeys)).toLowerCase();
|
|
397
|
+
if (!countries[code]) {
|
|
398
|
+
throw new Error(`Unknown country code "${code}". Supported: ${countryKeys.join(', ')}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const data = countries[code]!;
|
|
402
|
+
|
|
403
|
+
const addresses: Address[] = Array.from({ length: count }, () => ({
|
|
404
|
+
street: `${randomNumber(1, 200)} ${random(data.streetTypes)} ${random(data.streetNames)}`,
|
|
405
|
+
city: random(data.cities),
|
|
406
|
+
zip: data.zipFormat(),
|
|
407
|
+
state: random(data.regions),
|
|
408
|
+
country: data.name,
|
|
409
|
+
countryCode: code.toUpperCase(),
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
return { addresses };
|
|
413
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { randomInt } from 'crypto';
|
|
2
|
+
|
|
3
|
+
const MIN_LENGTH = 8;
|
|
4
|
+
const MAX_LENGTH = 128;
|
|
5
|
+
const MAX_COUNT = 20;
|
|
6
|
+
|
|
7
|
+
const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
8
|
+
const LOWER = 'abcdefghijklmnopqrstuvwxyz';
|
|
9
|
+
const DIGITS = '0123456789';
|
|
10
|
+
const SYMBOLS = '!@#$%^&*()-_=+[]{}|;:,.<>?';
|
|
11
|
+
|
|
12
|
+
const WORDLIST = [
|
|
13
|
+
'apple',
|
|
14
|
+
'brave',
|
|
15
|
+
'chair',
|
|
16
|
+
'dance',
|
|
17
|
+
'eagle',
|
|
18
|
+
'fancy',
|
|
19
|
+
'grace',
|
|
20
|
+
'house',
|
|
21
|
+
'ivory',
|
|
22
|
+
'joker',
|
|
23
|
+
'kneel',
|
|
24
|
+
'lemon',
|
|
25
|
+
'maple',
|
|
26
|
+
'noble',
|
|
27
|
+
'ocean',
|
|
28
|
+
'piano',
|
|
29
|
+
'queen',
|
|
30
|
+
'river',
|
|
31
|
+
'stone',
|
|
32
|
+
'tiger',
|
|
33
|
+
'ultra',
|
|
34
|
+
'vivid',
|
|
35
|
+
'waste',
|
|
36
|
+
'xenon',
|
|
37
|
+
'yacht',
|
|
38
|
+
'zebra',
|
|
39
|
+
'amber',
|
|
40
|
+
'blaze',
|
|
41
|
+
'coral',
|
|
42
|
+
'drift',
|
|
43
|
+
'ember',
|
|
44
|
+
'flora',
|
|
45
|
+
'globe',
|
|
46
|
+
'haven',
|
|
47
|
+
'input',
|
|
48
|
+
'jumpy',
|
|
49
|
+
'karma',
|
|
50
|
+
'lunar',
|
|
51
|
+
'magic',
|
|
52
|
+
'night',
|
|
53
|
+
'optic',
|
|
54
|
+
'proxy',
|
|
55
|
+
'quirk',
|
|
56
|
+
'radar',
|
|
57
|
+
'solar',
|
|
58
|
+
'trace',
|
|
59
|
+
'umbra',
|
|
60
|
+
'vapor',
|
|
61
|
+
'witch',
|
|
62
|
+
'xylem',
|
|
63
|
+
'yield',
|
|
64
|
+
'zippy',
|
|
65
|
+
'alpha',
|
|
66
|
+
'brisk',
|
|
67
|
+
'crane',
|
|
68
|
+
'depth',
|
|
69
|
+
'event',
|
|
70
|
+
'frost',
|
|
71
|
+
'grime',
|
|
72
|
+
'harsh',
|
|
73
|
+
'index',
|
|
74
|
+
'joint',
|
|
75
|
+
'knack',
|
|
76
|
+
'level',
|
|
77
|
+
'minus',
|
|
78
|
+
'nerve',
|
|
79
|
+
'onset',
|
|
80
|
+
'phase',
|
|
81
|
+
'quest',
|
|
82
|
+
'ratio',
|
|
83
|
+
'scope',
|
|
84
|
+
'trend',
|
|
85
|
+
'union',
|
|
86
|
+
'valve',
|
|
87
|
+
'weary',
|
|
88
|
+
'young',
|
|
89
|
+
'blunt',
|
|
90
|
+
'clean',
|
|
91
|
+
'drive',
|
|
92
|
+
'earth',
|
|
93
|
+
'fiber',
|
|
94
|
+
'grain',
|
|
95
|
+
'grind',
|
|
96
|
+
'horse',
|
|
97
|
+
'ideal',
|
|
98
|
+
'issue',
|
|
99
|
+
'judge',
|
|
100
|
+
'juice',
|
|
101
|
+
'knave',
|
|
102
|
+
'large',
|
|
103
|
+
'laser',
|
|
104
|
+
'light',
|
|
105
|
+
'limit',
|
|
106
|
+
'media',
|
|
107
|
+
'merit',
|
|
108
|
+
'metal',
|
|
109
|
+
'model',
|
|
110
|
+
'money',
|
|
111
|
+
'month',
|
|
112
|
+
'motor',
|
|
113
|
+
'mount',
|
|
114
|
+
'music',
|
|
115
|
+
'nerve',
|
|
116
|
+
'noise',
|
|
117
|
+
'north',
|
|
118
|
+
'noted',
|
|
119
|
+
'novel',
|
|
120
|
+
'nurse',
|
|
121
|
+
'oasis',
|
|
122
|
+
'offer',
|
|
123
|
+
'olive',
|
|
124
|
+
'omega',
|
|
125
|
+
'other',
|
|
126
|
+
'outer',
|
|
127
|
+
'owner',
|
|
128
|
+
'oxide',
|
|
129
|
+
'ozone',
|
|
130
|
+
'paint',
|
|
131
|
+
'panel',
|
|
132
|
+
'paper',
|
|
133
|
+
'patch',
|
|
134
|
+
'pause',
|
|
135
|
+
'peace',
|
|
136
|
+
'pearl',
|
|
137
|
+
'pedal',
|
|
138
|
+
'plain',
|
|
139
|
+
'plank',
|
|
140
|
+
'plant',
|
|
141
|
+
'plate',
|
|
142
|
+
'plaza',
|
|
143
|
+
'pluck',
|
|
144
|
+
'plumb',
|
|
145
|
+
'plume',
|
|
146
|
+
'plunge',
|
|
147
|
+
'point',
|
|
148
|
+
'polar',
|
|
149
|
+
'power',
|
|
150
|
+
'press',
|
|
151
|
+
'pride',
|
|
152
|
+
'prime',
|
|
153
|
+
'print',
|
|
154
|
+
'prism',
|
|
155
|
+
'prize',
|
|
156
|
+
'probe',
|
|
157
|
+
'prone',
|
|
158
|
+
'proof',
|
|
159
|
+
'prose',
|
|
160
|
+
'proud',
|
|
161
|
+
'prove',
|
|
162
|
+
'prowl',
|
|
163
|
+
'pulse',
|
|
164
|
+
'punch',
|
|
165
|
+
'pupil',
|
|
166
|
+
'purge',
|
|
167
|
+
'swift',
|
|
168
|
+
'sword',
|
|
169
|
+
'table',
|
|
170
|
+
'taint',
|
|
171
|
+
'tally',
|
|
172
|
+
'taste',
|
|
173
|
+
'teach',
|
|
174
|
+
'teeth',
|
|
175
|
+
'tempo',
|
|
176
|
+
'tense',
|
|
177
|
+
'tenth',
|
|
178
|
+
'terms',
|
|
179
|
+
'terra',
|
|
180
|
+
'thick',
|
|
181
|
+
'thing',
|
|
182
|
+
'think',
|
|
183
|
+
'thorn',
|
|
184
|
+
'three',
|
|
185
|
+
'throw',
|
|
186
|
+
'thumb',
|
|
187
|
+
'tidal',
|
|
188
|
+
'tight',
|
|
189
|
+
'tilde',
|
|
190
|
+
'timer',
|
|
191
|
+
'title',
|
|
192
|
+
'token',
|
|
193
|
+
'torch',
|
|
194
|
+
'total',
|
|
195
|
+
'touch',
|
|
196
|
+
'tough',
|
|
197
|
+
'tower',
|
|
198
|
+
'track',
|
|
199
|
+
'trade',
|
|
200
|
+
'trail',
|
|
201
|
+
'train',
|
|
202
|
+
'trait',
|
|
203
|
+
'tramp',
|
|
204
|
+
'tread',
|
|
205
|
+
'treat',
|
|
206
|
+
'trees',
|
|
207
|
+
'trial',
|
|
208
|
+
'tribe',
|
|
209
|
+
'trick',
|
|
210
|
+
'tried',
|
|
211
|
+
'troop',
|
|
212
|
+
'trove',
|
|
213
|
+
'truce',
|
|
214
|
+
'truck',
|
|
215
|
+
'truly',
|
|
216
|
+
'trunk',
|
|
217
|
+
'truth',
|
|
218
|
+
'tulip',
|
|
219
|
+
'tumor',
|
|
220
|
+
'tuner',
|
|
221
|
+
'tunic',
|
|
222
|
+
'tweak',
|
|
223
|
+
'twice',
|
|
224
|
+
'twirl',
|
|
225
|
+
'twist',
|
|
226
|
+
'typed',
|
|
227
|
+
'under',
|
|
228
|
+
'unify',
|
|
229
|
+
'unite',
|
|
230
|
+
'unity',
|
|
231
|
+
'until',
|
|
232
|
+
'upper',
|
|
233
|
+
'upset',
|
|
234
|
+
'urban',
|
|
235
|
+
'usage',
|
|
236
|
+
'valid',
|
|
237
|
+
'value',
|
|
238
|
+
'visor',
|
|
239
|
+
'vital',
|
|
240
|
+
'vivid',
|
|
241
|
+
'vocab',
|
|
242
|
+
'vocal',
|
|
243
|
+
'voice',
|
|
244
|
+
'voter',
|
|
245
|
+
'vault',
|
|
246
|
+
'venue',
|
|
247
|
+
'verse',
|
|
248
|
+
'watch',
|
|
249
|
+
'water',
|
|
250
|
+
'weave',
|
|
251
|
+
'wedge',
|
|
252
|
+
'weird',
|
|
253
|
+
'wheel',
|
|
254
|
+
'where',
|
|
255
|
+
'which',
|
|
256
|
+
'while',
|
|
257
|
+
'white',
|
|
258
|
+
'whole',
|
|
259
|
+
'wider',
|
|
260
|
+
'windy',
|
|
261
|
+
'wired',
|
|
262
|
+
'world',
|
|
263
|
+
'worse',
|
|
264
|
+
'worth',
|
|
265
|
+
'would',
|
|
266
|
+
'wound',
|
|
267
|
+
'write',
|
|
268
|
+
'wrong',
|
|
269
|
+
'wrote',
|
|
270
|
+
'years',
|
|
271
|
+
'yield',
|
|
272
|
+
'yours',
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
export interface PasswordResult {
|
|
276
|
+
passwords: string[];
|
|
277
|
+
type: string;
|
|
278
|
+
length: number;
|
|
279
|
+
strength: string;
|
|
280
|
+
entropy: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function computeStrength(entropy: number): string {
|
|
284
|
+
if (entropy < 40) return 'weak';
|
|
285
|
+
if (entropy < 60) return 'moderate';
|
|
286
|
+
if (entropy < 80) return 'strong';
|
|
287
|
+
return 'very_strong';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function generateRandom(length: number, charset: string): string {
|
|
291
|
+
return Array.from({ length }, () => charset[randomInt(charset.length)]).join('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function generatePassphrase(wordCount: number, separator: string): string {
|
|
295
|
+
return Array.from({ length: wordCount }, () => WORDLIST[randomInt(WORDLIST.length)]).join(separator);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generates one or more passwords using random characters or passphrase mode.
|
|
300
|
+
*
|
|
301
|
+
* @param type - Generation mode: "random" or "passphrase"
|
|
302
|
+
* @param length - Password length for random mode (8–128), or word count for passphrase mode (3–10)
|
|
303
|
+
* @param options - Character set options: uppercase, lowercase, digits, symbols, exclude, count, separator
|
|
304
|
+
* @returns Generated passwords with type, length, strength and entropy
|
|
305
|
+
* @throws Error if no charset is active, length is out of range, or count exceeds the maximum
|
|
306
|
+
*/
|
|
307
|
+
export default function password(
|
|
308
|
+
type: string = 'random',
|
|
309
|
+
length: number = 16,
|
|
310
|
+
options: {
|
|
311
|
+
uppercase?: boolean;
|
|
312
|
+
lowercase?: boolean;
|
|
313
|
+
digits?: boolean;
|
|
314
|
+
symbols?: boolean;
|
|
315
|
+
exclude?: string;
|
|
316
|
+
count?: number;
|
|
317
|
+
separator?: string;
|
|
318
|
+
} = {},
|
|
319
|
+
): PasswordResult {
|
|
320
|
+
const {
|
|
321
|
+
uppercase = true,
|
|
322
|
+
lowercase = true,
|
|
323
|
+
digits = true,
|
|
324
|
+
symbols = false,
|
|
325
|
+
exclude = '',
|
|
326
|
+
count = 1,
|
|
327
|
+
separator = '-',
|
|
328
|
+
} = options;
|
|
329
|
+
|
|
330
|
+
if (count < 1 || count > MAX_COUNT) {
|
|
331
|
+
throw new Error(`Count must be between 1 and ${MAX_COUNT}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type === 'passphrase') {
|
|
335
|
+
const wordCount = Math.max(3, Math.min(10, length));
|
|
336
|
+
const entropy = Math.log2(Math.pow(WORDLIST.length, wordCount));
|
|
337
|
+
const passwords = Array.from({ length: count }, () => generatePassphrase(wordCount, separator));
|
|
338
|
+
return {
|
|
339
|
+
passwords,
|
|
340
|
+
type: 'passphrase',
|
|
341
|
+
length: wordCount,
|
|
342
|
+
strength: computeStrength(entropy),
|
|
343
|
+
entropy: Math.round(entropy * 10) / 10,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (type !== 'random') {
|
|
348
|
+
throw new Error('Type must be "random" or "passphrase"');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isNaN(length) || length < MIN_LENGTH || length > MAX_LENGTH) {
|
|
352
|
+
throw new Error(`Length must be between ${MIN_LENGTH} and ${MAX_LENGTH}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let charset = '';
|
|
356
|
+
if (uppercase) charset += UPPER;
|
|
357
|
+
if (lowercase) charset += LOWER;
|
|
358
|
+
if (digits) charset += DIGITS;
|
|
359
|
+
if (symbols) charset += SYMBOLS;
|
|
360
|
+
|
|
361
|
+
if (exclude) {
|
|
362
|
+
for (const char of exclude) {
|
|
363
|
+
charset = charset.replaceAll(char, '');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (charset.length === 0) {
|
|
368
|
+
throw new Error('At least one character set must be enabled');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const entropy = Math.log2(Math.pow(charset.length, length));
|
|
372
|
+
const passwords = Array.from({ length: count }, () => generateRandom(length, charset));
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
passwords,
|
|
376
|
+
type: 'random',
|
|
377
|
+
length,
|
|
378
|
+
strength: computeStrength(entropy),
|
|
379
|
+
entropy: Math.round(entropy * 10) / 10,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { random, randomNumber } from '../../utils/helpers.js';
|
|
2
|
+
import addressFn from './address.js';
|
|
2
3
|
|
|
3
4
|
interface Person {
|
|
4
5
|
name: string;
|
|
@@ -47,21 +48,6 @@ export default function personal(): Record<string, unknown> {
|
|
|
47
48
|
|
|
48
49
|
const jobs = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Doctor'];
|
|
49
50
|
const hobbies = ['Reading', 'Traveling', 'Gaming', 'Cooking', 'Fitness', 'Music', 'Photography', 'Writing'];
|
|
50
|
-
const cities = [
|
|
51
|
-
'New York',
|
|
52
|
-
'Paris',
|
|
53
|
-
'London',
|
|
54
|
-
'Madrid',
|
|
55
|
-
'Berlin',
|
|
56
|
-
'Rome',
|
|
57
|
-
'Tokyo',
|
|
58
|
-
'Los Angeles',
|
|
59
|
-
'Sydney',
|
|
60
|
-
'São Paulo',
|
|
61
|
-
'Toronto',
|
|
62
|
-
];
|
|
63
|
-
const streets = ['Main St', '2nd Ave', 'Broadway', 'Park Lane', 'Elm St', 'Sunset Blvd', 'Maple St', 'Highland Rd'];
|
|
64
|
-
|
|
65
51
|
const card = Array.from({ length: 4 }, () => randomNumber(1000, 9999)).join(' ');
|
|
66
52
|
const cvc = randomNumber(100, 999);
|
|
67
53
|
const expiration = `${String(randomNumber(1, 12)).padStart(2, '0')}/${(new Date().getFullYear() + randomNumber(0, 3)).toString().slice(-2)}`;
|
|
@@ -69,6 +55,8 @@ export default function personal(): Record<string, unknown> {
|
|
|
69
55
|
const person = random(people);
|
|
70
56
|
const social = person.social;
|
|
71
57
|
const country = person.country;
|
|
58
|
+
const countryCodes: Record<string, string> = { US: 'us', FR: 'fr', UK: 'uk', ES: 'es', DE: 'de' };
|
|
59
|
+
const addrCountry = countryCodes[country] ?? 'us';
|
|
72
60
|
const countryInfo = countries[country]!;
|
|
73
61
|
const phone = countryInfo.tel;
|
|
74
62
|
const lang = countryInfo.lang;
|
|
@@ -129,7 +117,7 @@ export default function personal(): Record<string, unknown> {
|
|
|
129
117
|
card,
|
|
130
118
|
cvc,
|
|
131
119
|
expiration,
|
|
132
|
-
address:
|
|
120
|
+
address: addressFn(addrCountry).addresses[0]!,
|
|
133
121
|
birthday,
|
|
134
122
|
civil_status: civilStatus,
|
|
135
123
|
children,
|
package/src/modules/v4/token.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'crypto';
|
|
1
|
+
import { randomBytes, randomInt } from 'crypto';
|
|
2
2
|
import { v4 } from 'uuid';
|
|
3
3
|
|
|
4
4
|
import { MAX_TOKEN_LENGTH, MIN_TOKEN_LENGTH } from '../../constants.js';
|
|
@@ -20,11 +20,8 @@ export default function token(len: number, type: string = 'alphanum'): string {
|
|
|
20
20
|
throw new Error('Length cannot exceed 4096');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const genToken = (chars: string, length: number): string =>
|
|
24
|
-
|
|
25
|
-
return chars[Math.floor(Math.random() * chars.length)];
|
|
26
|
-
}).join('');
|
|
27
|
-
};
|
|
23
|
+
const genToken = (chars: string, length: number): string =>
|
|
24
|
+
Array.from({ length }, () => chars[randomInt(chars.length)]).join('');
|
|
28
25
|
|
|
29
26
|
const tokenTypes: Record<string, () => string> = {
|
|
30
27
|
alpha: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
|
|
@@ -40,7 +37,11 @@ export default function token(len: number, type: string = 'alphanum'): string {
|
|
|
40
37
|
num: () => genToken('0123456789', len),
|
|
41
38
|
punct: () => genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
|
|
42
39
|
urlsafe: () => genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
|
|
43
|
-
uuid: () =>
|
|
40
|
+
uuid: () => {
|
|
41
|
+
let result = '';
|
|
42
|
+
while (result.length < len) result += v4().replace(/-/g, '');
|
|
43
|
+
return result.slice(0, len);
|
|
44
|
+
},
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const lowerType = type.toLowerCase();
|
package/src/modules/v4.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as address } from './v4/address.js';
|
|
1
2
|
export { default as agent } from './v4/agent.js';
|
|
2
3
|
export * as algorithms from './v4/algorithms.js';
|
|
3
4
|
export { default as captcha } from './v4/captcha.js';
|
|
@@ -13,6 +14,7 @@ export { default as hyperplanning } from './v4/hyperplanning.js';
|
|
|
13
14
|
export { default as ip } from './v4/ip.js';
|
|
14
15
|
export { default as levenshtein } from './v4/levenshtein.js';
|
|
15
16
|
export { default as palette } from './v4/palette.js';
|
|
17
|
+
export { default as password } from './v4/password.js';
|
|
16
18
|
export { default as personal } from './v4/personal.js';
|
|
17
19
|
export { default as placeholder } from './v4/placeholder.js';
|
|
18
20
|
export { default as qrcode } from './v4/qrcode.js';
|
package/src/routes/get.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { type Request, type Response, Router } from 'express';
|
|
|
3
3
|
import { env } from '../config/env.js';
|
|
4
4
|
import { versions } from '../config/versions.js';
|
|
5
5
|
import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
|
|
6
|
+
import type { AddressResult } from '../modules/v4/address.js';
|
|
6
7
|
import type { UserAgentResult } from '../modules/v4/agent.js';
|
|
7
8
|
import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
|
|
8
9
|
import type { ColorResult } from '../modules/v4/color.js';
|
|
9
10
|
import type { IpResult } from '../modules/v4/ip.js';
|
|
11
|
+
import type { PasswordResult } from '../modules/v4/password.js';
|
|
10
12
|
import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
|
|
11
13
|
import { chatStorage, ipLimits } from '../storage/index.js';
|
|
12
14
|
import { since } from '../utils/helpers.js';
|
|
@@ -176,6 +178,27 @@ router.get('/:version/convert', (req: Request, res: Response) => {
|
|
|
176
178
|
}
|
|
177
179
|
});
|
|
178
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
|
+
|
|
179
202
|
// Echo request headers
|
|
180
203
|
router.get('/:version/headers', (req: Request, res: Response) => {
|
|
181
204
|
const redacted = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
|
|
@@ -376,6 +399,41 @@ router.get('/:version/palette', (req: Request, res: Response) => {
|
|
|
376
399
|
}
|
|
377
400
|
});
|
|
378
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
|
+
|
|
379
437
|
// Generate personal data
|
|
380
438
|
router.get('/:version/personal', (req: Request, res: Response) => {
|
|
381
439
|
try {
|
|
@@ -643,6 +643,61 @@ describe('GET /v4/agent', () => {
|
|
|
643
643
|
});
|
|
644
644
|
});
|
|
645
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
|
+
|
|
646
701
|
describe('Prototype access on dynamic endpoints', () => {
|
|
647
702
|
test('algorithms?method=toString returns 400', async () => {
|
|
648
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
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import password from '../../src/modules/v4/password.js';
|
|
5
|
+
|
|
6
|
+
describe('password', () => {
|
|
7
|
+
describe('random mode', () => {
|
|
8
|
+
test('default returns one password of length 16', () => {
|
|
9
|
+
const r = password();
|
|
10
|
+
assert.equal(r.passwords.length, 1);
|
|
11
|
+
assert.equal(r.passwords[0]!.length, 16);
|
|
12
|
+
assert.equal(r.type, 'random');
|
|
13
|
+
assert.equal(r.length, 16);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('length is respected', () => {
|
|
17
|
+
const r = password('random', 32);
|
|
18
|
+
assert.equal(r.passwords[0]!.length, 32);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('no symbols when symbols=false', () => {
|
|
22
|
+
const r = password('random', 64, { symbols: false });
|
|
23
|
+
assert.ok(!/[!@#$%^&*()\-_=+[\]{}|;:,.<>?]/.test(r.passwords[0]!));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('only digits when only digits enabled', () => {
|
|
27
|
+
const r = password('random', 16, { uppercase: false, lowercase: false, digits: true, symbols: false });
|
|
28
|
+
assert.ok(/^\d+$/.test(r.passwords[0]!));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('exclude chars are absent from password', () => {
|
|
32
|
+
const r = password('random', 64, { exclude: 'aeiou' });
|
|
33
|
+
assert.ok(!/[aeiou]/.test(r.passwords[0]!));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('count=5 returns 5 passwords', () => {
|
|
37
|
+
const r = password('random', 16, { count: 5 });
|
|
38
|
+
assert.equal(r.passwords.length, 5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('entropy and strength are present', () => {
|
|
42
|
+
const r = password();
|
|
43
|
+
assert.ok(typeof r.entropy === 'number');
|
|
44
|
+
assert.ok(['weak', 'moderate', 'strong', 'very_strong'].includes(r.strength));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('passphrase mode', () => {
|
|
49
|
+
test('returns words joined by separator', () => {
|
|
50
|
+
const r = password('passphrase', 4, { separator: '-' });
|
|
51
|
+
assert.equal(r.type, 'passphrase');
|
|
52
|
+
const words = r.passwords[0]!.split('-');
|
|
53
|
+
assert.equal(words.length, 4);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('custom separator is used', () => {
|
|
57
|
+
const r = password('passphrase', 3, { separator: '.' });
|
|
58
|
+
assert.ok(r.passwords[0]!.includes('.'));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('errors', () => {
|
|
63
|
+
test('length below 8 throws', () => {
|
|
64
|
+
assert.throws(() => password('random', 4), /Length must be between/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('length above 128 throws', () => {
|
|
68
|
+
assert.throws(() => password('random', 200), /Length must be between/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('no charset active throws', () => {
|
|
72
|
+
assert.throws(
|
|
73
|
+
() => password('random', 16, { uppercase: false, lowercase: false, digits: false, symbols: false }),
|
|
74
|
+
/At least one character set/,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('count above 20 throws', () => {
|
|
79
|
+
assert.throws(() => password('random', 16, { count: 21 }), /Count must be between/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('invalid type throws', () => {
|
|
83
|
+
assert.throws(() => password('invalid'), /Type must be/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|