@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 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
- [![Version](https://custom-icon-badges.demolab.com/badge/Version%20:-v4.6.0-6479ee?logo=api.sylvain.sh&labelColor=23272A)](https://github.com/20syldev/api/releases/latest)
5
+ [![Version](https://custom-icon-badges.demolab.com/badge/Version%20:-v4.7.0-6479ee?logo=api.sylvain.sh&labelColor=23272A)](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.6.0 build
35
+ > @20syldev/api@4.7.0 build
36
36
  > tsc
37
37
 
38
- > @20syldev/api@4.6.0 start
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.6.0",
2
+ "version": "4.7.0",
3
3
  "name": "@20syldev/api",
4
4
  "description": "Node.js API with multiple features. Check the documentation at https://docs.sylvain.sh",
5
5
  "main": "dist/app.js",
@@ -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: `${randomNumber(1, 9999)} ${random(streets)}, ${random(cities)}`,
120
+ address: addressFn(addrCountry).addresses[0]!,
133
121
  birthday,
134
122
  civil_status: civilStatus,
135
123
  children,
@@ -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
- return Array.from({ length }, () => {
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: () => v4().replace(/-/g, '').slice(0, len),
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
+ });