@zero-server/sdk 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -437
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +460 -460
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +136 -136
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +254 -254
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/cluster.d.ts +75 -75
  113. package/types/env.d.ts +80 -80
  114. package/types/errors.d.ts +316 -316
  115. package/types/fetch.d.ts +43 -43
  116. package/types/grpc.d.ts +432 -432
  117. package/types/index.d.ts +384 -384
  118. package/types/lifecycle.d.ts +60 -60
  119. package/types/middleware.d.ts +320 -320
  120. package/types/observe.d.ts +304 -304
  121. package/types/orm.d.ts +1887 -1887
  122. package/types/request.d.ts +109 -109
  123. package/types/response.d.ts +157 -157
  124. package/types/router.d.ts +78 -78
  125. package/types/sse.d.ts +78 -78
  126. package/types/websocket.d.ts +126 -126
@@ -1,1186 +1,1186 @@
1
- 'use strict';
2
-
3
- /**
4
- * @module seed/fake
5
- * @description Extensible fake data generator.
6
- *
7
- * Key capabilities:
8
- * • Seeded / reproducible output — Fake.seed(42)
9
- * • Guaranteed-unique values — Fake.unique(() => Fake.email())
10
- * • Multi-locale names — Fake.firstName({ locale: 'ja', sex: 'female' })
11
- * • Rich phone formats — Fake.phone({ countryCode: 'DE', format: 'international' })
12
- * • Configurable emails — Fake.email({ provider: 'company.io' })
13
- * • Flexible usernames — Fake.username({ style: 'underscore' })
14
- * • Fixed-length numeric strings — Fake.numericString(8)
15
- * • Person: job titles, bio, zodiac, gender, prefix/suffix
16
- * • Location: city, country, state, address, lat/lng, timezone
17
- * • Commerce: product, company, category, price, industry
18
- * • Internet: password, mac, port, userAgent, domain, ipv6, slug
19
- * • All original generators retained (100 % backward-compatible)
20
- */
21
-
22
- const crypto = require('crypto');
23
- const { rand, seed: _seedFn, getSeed } = require('./rng');
24
- const { UniqueTracker } = require('./unique');
25
-
26
- // -- Data modules ------------------------------------------------
27
- const { NAMES, LOCALES } = require('./data/names');
28
- const { PHONE_BY_COUNTRY, COUNTRY_CODES } = require('./data/phone');
29
- const {
30
- EMAIL_PROVIDERS, SAFE_DOMAINS, TLDS,
31
- DOMAIN_ADJECTIVES, DOMAIN_NOUNS,
32
- USERNAME_SEPARATORS, USER_AGENTS, MAC_OUIS,
33
- CHARSET_LOWERCASE, CHARSET_UPPERCASE, CHARSET_DIGITS, CHARSET_SPECIAL,
34
- } = require('./data/internet');
35
- const {
36
- LOREM_WORDS, HACKER_ADJECTIVES, HACKER_NOUNS, HACKER_VERBS,
37
- ADJECTIVES, NOUNS,
38
- } = require('./data/words');
39
- const {
40
- NAME_PREFIXES, NAME_SUFFIXES,
41
- JOB_DESCRIPTORS, JOB_AREAS, JOB_TYPES, JOB_TITLES,
42
- ZODIAC_SIGNS, GENDERS, BIO_ADJECTIVES, BIO_NOUNS, BIO_PHRASES,
43
- BLOOD_TYPES,
44
- } = require('./data/person');
45
- const {
46
- CITIES, COUNTRIES, US_STATES,
47
- STREET_TYPES, STREET_NAMES, ZIP_PATTERNS, TIMEZONES,
48
- } = require('./data/locations');
49
- const {
50
- PRODUCT_ADJECTIVES, PRODUCT_MATERIALS, PRODUCT_NOUNS,
51
- CATEGORIES, DEPARTMENTS,
52
- COMPANY_ADJECTIVES, COMPANY_NOUNS, COMPANY_SUFFIXES, INDUSTRIES,
53
- CATCH_PHRASE_ADJECTIVES, CATCH_PHRASE_NOUNS,
54
- } = require('./data/commerce');
55
-
56
- // ================================================================
57
- // Internal helpers
58
- // ================================================================
59
-
60
- /** @private Pick a random element from an array using the active RNG. */
61
- function _pick(arr) {
62
- return arr[Math.floor(rand() * arr.length)];
63
- }
64
-
65
- /** @private Pick n unique-indexed elements from an array (no repeats). */
66
- function _pickMany(arr, n) {
67
- const copy = arr.slice();
68
- for (let i = copy.length - 1; i > 0; i--) {
69
- const j = Math.floor(rand() * (i + 1));
70
- [copy[i], copy[j]] = [copy[j], copy[i]];
71
- }
72
- return copy.slice(0, Math.min(n, arr.length));
73
- }
74
-
75
- /** @private Random integer in [min, max] inclusive. */
76
- function _int(min, max) {
77
- return Math.floor(rand() * (max - min + 1)) + min;
78
- }
79
-
80
- /**
81
- * @private
82
- * Expand a phone / zip template string.
83
- * # → digit 0–9 N → digit 1–9 A → uppercase letter
84
- */
85
- function _expandTemplate(template) {
86
- return template.replace(/[#NA]/g, c => {
87
- if (c === '#') return _int(0, 9);
88
- if (c === 'N') return _int(1, 9);
89
- return String.fromCharCode(_int(65, 90)); // c === 'A'
90
- });
91
- }
92
-
93
- /** @private Resolve name pool for a locale, falling back to 'en'. */
94
- function _namePool(locale) {
95
- return NAMES[locale] || NAMES.en;
96
- }
97
-
98
- /** @private Derive sex ('male'|'female') from options or random. */
99
- function _resolveSex(options = {}) {
100
- const { sex } = options;
101
- if (sex === 'male' || sex === 'female') return sex;
102
- return rand() > 0.5 ? 'male' : 'female';
103
- }
104
-
105
- /** @private UUID v4 fallback for environments without crypto.randomUUID. */
106
- function _uuid() {
107
- if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
108
- const b = crypto.randomBytes(16);
109
- b[6] = (b[6] & 0x0f) | 0x40;
110
- b[8] = (b[8] & 0x3f) | 0x80;
111
- const h = b.toString('hex');
112
- return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
113
- }
114
-
115
- // ================================================================
116
- // Fake class
117
- // ================================================================
118
-
119
- /**
120
- * Static fake data generator with seeded RNG, locale support, and
121
- * built-in uniqueness guarantees.
122
- *
123
- * All methods are **100 % backward compatible** with the original Fake class.
124
- *
125
- * @example
126
- * // Reproducible output
127
- * Fake.seed(42);
128
- * Fake.firstName(); // always the same value
129
- *
130
- * // Guaranteed unique emails
131
- * const emails = Array.from({ length: 200 }, () =>
132
- * Fake.unique(() => Fake.email())
133
- * );
134
- *
135
- * // Locale-specific names
136
- * Fake.firstName({ locale: 'ja', sex: 'female' }); // e.g. 'Hina'
137
- */
138
- class Fake
139
- {
140
- // -- Seeding & Uniqueness ------------------------------------------------
141
-
142
- /**
143
- * Set a deterministic seed so all subsequent calls produce the same output.
144
- * Pass `null` or `undefined` to reset to Math.random.
145
- *
146
- * @param {number|string|null} [value] - Value to set.
147
- * @returns {number|null} The resolved numeric seed.
148
- *
149
- * @example
150
- * Fake.seed(42);
151
- * Fake.firstName(); // reproducible
152
- * Fake.seed(null); // back to random
153
- */
154
- static seed(value) { return _seedFn(value); }
155
-
156
- /** @returns {number|null} Active seed, or null if using Math.random. */
157
- static getSeed() { return getSeed(); }
158
-
159
- /**
160
- * Generate a guaranteed-unique value within a namespace.
161
- *
162
- * @param {() => any} fn - Generator function.
163
- * @param {object} [options] - Configuration options.
164
- * @param {string} [options.key] - Namespace key (defaults to function name).
165
- * @param {number} [options.maxAttempts] - Retry limit (default 1000).
166
- * @returns {any} The unique generated value.
167
- *
168
- * @example
169
- * const email = Fake.unique(() => Fake.email(), { key: 'email' });
170
- */
171
- static unique(fn, options = {}) {
172
- const key = options.key || fn.name || 'default';
173
- return Fake._tracker.generate(key, fn, options.maxAttempts);
174
- }
175
-
176
- /**
177
- * Reset unique-value tracking.
178
- * @param {string} [key] - Clear only this namespace, or all if omitted.
179
- */
180
- static resetUnique(key) { Fake._tracker.reset(key); }
181
-
182
- /**
183
- * How many unique values have been generated for a namespace.
184
- * @param {string} key - Cache or storage key.
185
- * @returns {number} Count of unique values tracked for the key.
186
- */
187
- static uniqueCount(key) { return Fake._tracker.seen(key); }
188
-
189
- // -- Names --------------------------------------------------------------
190
-
191
- /**
192
- * Random first name.
193
- *
194
- * @param {object} [options] - Configuration options.
195
- * @param {'male'|'female'} [options.sex] - Constrain to a sex.
196
- * @param {string} [options.locale='en'] - Locale code (en, es, fr, de, …).
197
- * @param {boolean} [options.unique=false] - Guarantee uniqueness per locale+sex.
198
- * @returns {string} A random first name.
199
- */
200
- static firstName(options = {}) {
201
- const locale = options.locale || 'en';
202
- const sex = _resolveSex(options);
203
- const pool = _namePool(locale);
204
- const fn = () => _pick(pool[sex]);
205
- if (options.unique) {
206
- return Fake.unique(fn, { key: `firstName_${locale}_${sex}` });
207
- }
208
- return fn();
209
- }
210
-
211
- /**
212
- * Random last name.
213
- *
214
- * @param {object} [options] - Configuration options.
215
- * @param {string} [options.locale='en'] - Locale code (e.g. "en").
216
- * @param {boolean} [options.unique=false] - Whether to ensure unique values.
217
- * @returns {string} A random last name.
218
- */
219
- static lastName(options = {}) {
220
- const locale = options.locale || 'en';
221
- const pool = _namePool(locale);
222
- const fn = () => _pick(pool.last);
223
- if (options.unique) return Fake.unique(fn, { key: `lastName_${locale}` });
224
- return fn();
225
- }
226
-
227
- /**
228
- * Random middle name (falls back to first-name pool if locale lacks one).
229
- *
230
- * @param {object} [options] - Configuration options.
231
- * @param {'male'|'female'} [options.sex] - Biological sex (male/female).
232
- * @param {string} [options.locale='en'] - Locale code (e.g. "en").
233
- * @returns {string} A random middle name.
234
- */
235
- static middleName(options = {}) {
236
- // Reuse first-name pool since most locales share it
237
- return Fake.firstName({ sex: options.sex, locale: options.locale });
238
- }
239
-
240
- /**
241
- * Random full name.
242
- *
243
- * @param {object} [options] - Configuration options.
244
- * @param {'male'|'female'} [options.sex] - Biological sex (male/female).
245
- * @param {string} [options.locale='en'] - Locale code (e.g. "en").
246
- * @param {boolean} [options.prefix=false] - Include name prefix (Mr., Dr., …).
247
- * @param {boolean} [options.middle=false] - Include a middle name.
248
- * @param {boolean} [options.suffix=false] - Include credential suffix (PhD, Jr., …).
249
- * @param {string} [options.firstName] - Override the first name.
250
- * @param {string} [options.lastName] - Override the last name.
251
- * @param {boolean} [options.unique=false] - Guarantee uniqueness per locale.
252
- * @returns {string} A full name string.
253
- */
254
- static fullName(options = {}) {
255
- const locale = options.locale || 'en';
256
- const sex = _resolveSex(options);
257
-
258
- const fn = () => {
259
- const parts = [];
260
- if (options.prefix) parts.push(Fake.namePrefix({ sex }));
261
- parts.push(options.firstName || Fake.firstName({ sex, locale }));
262
- if (options.middle) parts.push(Fake.middleName({ sex, locale }));
263
- parts.push(options.lastName || Fake.lastName({ locale }));
264
- if (options.suffix) parts.push(Fake.nameSuffix());
265
- return parts.join(' ');
266
- };
267
-
268
- if (options.unique) return Fake.unique(fn, { key: `fullName_${locale}` });
269
- return fn();
270
- }
271
-
272
- /**
273
- * Name prefix (Mr., Ms., Dr., Prof., …).
274
- *
275
- * @param {object} [options] - Configuration options.
276
- * @param {'male'|'female'|'neutral'} [options.sex] - Biological sex (male/female).
277
- * @returns {string} A title prefix like 'Mr.' or 'Dr.'.
278
- */
279
- static namePrefix(options = {}) {
280
- const sex = options.sex === 'male' ? 'male'
281
- : options.sex === 'female' ? 'female'
282
- : options.sex === 'neutral' ? 'neutral'
283
- : rand() > 0.5 ? 'male'
284
- : 'female';
285
- return _pick(NAME_PREFIXES[sex]);
286
- }
287
-
288
- /** Random name suffix (Jr., Sr., PhD, MD, …). */
289
- static nameSuffix() { return _pick(NAME_SUFFIXES); }
290
-
291
- /** All supported locale codes. */
292
- static locales() { return LOCALES.slice(); }
293
-
294
- // -- Phone Numbers ------------------------------------------------------
295
-
296
- /**
297
- * Random phone number.
298
- *
299
- * @param {object} [options] - Configuration options.
300
- * @param {string} [options.countryCode] - ISO 3166-1 alpha-2 (e.g. 'US', 'DE').
301
- * Defaults to a random country.
302
- * @param {'human'|'national'|'international'} [options.format='human'] - Output format.
303
- * @param {boolean} [options.unique=false] - Whether to ensure unique values.
304
- * @returns {string} Formatted string.
305
- *
306
- * @example
307
- * Fake.phone(); // '(555) 123-4567'
308
- * Fake.phone({ countryCode: 'DE' }); // '0174 #######'
309
- * Fake.phone({ format: 'international' }); // '+1 555 123 4567'
310
- */
311
- static phone(options = {}) {
312
- const code = (options.countryCode || 'US').toUpperCase();
313
- const country = PHONE_BY_COUNTRY[code] || PHONE_BY_COUNTRY['US'];
314
- // Default to 'national' so bare Fake.phone() produces the classic
315
- // (###) ###-#### format and remains backward compatible.
316
- const style = options.format || 'national';
317
- const formats = country.formats[style] || country.formats.national;
318
- const fn = () => _expandTemplate(_pick(formats));
319
- if (options.unique) return Fake.unique(fn, { key: `phone_${code}_${style}` });
320
- return fn();
321
- }
322
-
323
- /**
324
- * All supported phone country codes.
325
- * @returns {string[]} Array of ISO 3166-1 alpha-2 codes.
326
- */
327
- static phoneCodes() { return COUNTRY_CODES.slice(); }
328
-
329
- // -- Email --------------------------------------------------------------
330
-
331
- /**
332
- * Random email address.
333
- *
334
- * @param {object} [options] - Configuration options.
335
- * @param {string} [options.firstName] - Override the local-part first name.
336
- * @param {string} [options.lastName] - Override the local-part last name.
337
- * @param {string} [options.provider] - Force a provider domain.
338
- * @param {boolean} [options.safe=false] - Use only example./test. safe domains.
339
- * @param {string} [options.locale='en'] - Locale for random name generation.
340
- * @param {boolean} [options.unique=false] - Whether to ensure unique values.
341
- * @returns {string} An email address string.
342
- *
343
- * @example
344
- * Fake.email();
345
- * Fake.email({ provider: 'company.io', unique: true });
346
- * Fake.email({ safe: true }); // only example.com/test.com domains
347
- */
348
- static email(options = {}) {
349
- const locale = options.locale || 'en';
350
- const fn = () => {
351
- const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
352
- .replace(/[^a-z0-9]/g, '');
353
- const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
354
- .replace(/[^a-z0-9]/g, '');
355
- const num = _int(1, 999);
356
- const domain = options.provider
357
- ? options.provider
358
- : options.safe
359
- ? _pick(SAFE_DOMAINS)
360
- : _pick(EMAIL_PROVIDERS);
361
-
362
- const patterns = [
363
- `${first}.${last}${num}@${domain}`,
364
- `${first}${last}@${domain}`,
365
- `${first}_${last}@${domain}`,
366
- `${first}${num}@${domain}`,
367
- `${first[0]}${last}@${domain}`,
368
- ];
369
- return _pick(patterns);
370
- };
371
-
372
- if (options.unique) return Fake.unique(fn, { key: `email_${options.provider || 'any'}` });
373
- return fn();
374
- }
375
-
376
- // -- Username -----------------------------------------------------------
377
-
378
- /**
379
- * Random username.
380
- *
381
- * @param {object} [options] - Configuration options.
382
- * @param {string} [options.firstName] - Override first name component.
383
- * @param {string} [options.lastName] - Override last name component.
384
- * @param {'dot'|'underscore'|'none'|'random'} [options.style='random'] - Style variant.
385
- * @param {boolean} [options.numbers=true] - Append a random number suffix.
386
- * @param {string} [options.locale='en'] - Locale code (e.g. "en").
387
- * @param {boolean} [options.unique=false] - Whether to ensure unique values.
388
- * @returns {string} A username string.
389
- *
390
- * @example
391
- * Fake.username(); // 'alice_smith42'
392
- * Fake.username({ style: 'dot' }); // 'john.doe91'
393
- * Fake.username({ style: 'none', numbers: false }); // 'janedoe'
394
- */
395
- static username(options = {}) {
396
- const locale = options.locale || 'en';
397
- const style = options.style || 'random';
398
- const useNums = options.numbers !== false;
399
-
400
- const fn = () => {
401
- const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
402
- .replace(/[^a-z0-9]/g, '');
403
- const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
404
- .replace(/[^a-z0-9]/g, '');
405
-
406
- let sep;
407
- if (style === 'dot') sep = '.';
408
- else if (style === 'underscore') sep = '_';
409
- else if (style === 'none') sep = '';
410
- else sep = _pick(USERNAME_SEPARATORS);
411
-
412
- const base = `${first}${sep}${last}`;
413
- const num = useNums ? _int(1, 9999) : '';
414
- return `${base}${num}`;
415
- };
416
-
417
- if (options.unique) return Fake.unique(fn, { key: `username_${style}` });
418
- return fn();
419
- }
420
-
421
- // -- Numbers ------------------------------------------------------------
422
-
423
- /**
424
- * Random integer in [min, max].
425
- *
426
- * @param {number|object} [min=0] - Min bound, or options object.
427
- * @param {number} [max=100] - Maximum value.
428
- * @returns {number} A random integer.
429
- */
430
- static integer(min = 0, max = 100) {
431
- if (typeof min === 'object' && min !== null) {
432
- const opts = min;
433
- return _int(opts.min ?? 0, opts.max ?? 100);
434
- }
435
- return _int(Math.floor(min), Math.floor(max));
436
- }
437
-
438
- /**
439
- * Random float in [min, max].
440
- *
441
- * @param {number|object} [min=0] - Minimum value.
442
- * @param {number} [max=100] - Maximum value.
443
- * @param {number} [decimals=2] - Decimal places.
444
- * @returns {number} A random float.
445
- */
446
- static float(min = 0, max = 100, decimals = 2) {
447
- if (typeof min === 'object' && min !== null) {
448
- const opts = min;
449
- return Number((rand() * ((opts.max ?? 100) - (opts.min ?? 0)) + (opts.min ?? 0))
450
- .toFixed(opts.decimals ?? 2));
451
- }
452
- return Number((rand() * (max - min) + min).toFixed(decimals));
453
- }
454
-
455
- /**
456
- * Random numeric string with an exact number of digits.
457
- * Leading zeros are preserved (unlike integer()).
458
- *
459
- * @param {number} [length=6] - String length.
460
- * @param {object} [options] - Configuration options.
461
- * @param {boolean} [options.leadingZeros=true] - Allow a leading zero.
462
- * @param {string} [options.separator=''] - Insert separator every N digits.
463
- * @param {number} [options.groupSize=3] - Group size when using separator.
464
- * @returns {string} A fixed-length digit string.
465
- *
466
- * @example
467
- * Fake.numericString(6); // '047283'
468
- * Fake.numericString(16, { separator: '-', groupSize: 4 }); // '1234-5678-9012-3456'
469
- */
470
- static numericString(length = 6, options = {}) {
471
- const allowLeading = options.leadingZeros !== false;
472
- let str = '';
473
- for (let i = 0; i < length; i++) {
474
- const min = (!allowLeading && i === 0) ? 1 : 0;
475
- str += _int(min, 9);
476
- }
477
- if (options.separator) {
478
- const gs = options.groupSize || 3;
479
- const re = new RegExp(`(.{${gs}})(?=.)`, 'g');
480
- str = str.replace(re, `$1${options.separator}`);
481
- }
482
- return str;
483
- }
484
-
485
- /**
486
- * Random alphanumeric string of `length` characters.
487
- *
488
- * @param {number} [length=10] - String length.
489
- * @param {object} [options] - Configuration options.
490
- * @param {boolean} [options.uppercase=false] - Whether to use uppercase.
491
- * @param {boolean} [options.letters=true] - Include letter characters.
492
- * @param {boolean} [options.digits=true] - Include digit characters.
493
- * @returns {string} An alphanumeric string.
494
- */
495
- static alphanumeric(length = 10, options = {}) {
496
- let pool = '';
497
- if (options.letters !== false) {
498
- pool += options.uppercase ? CHARSET_UPPERCASE : CHARSET_LOWERCASE;
499
- }
500
- if (options.digits !== false) pool += CHARSET_DIGITS;
501
- if (!pool) pool = CHARSET_LOWERCASE + CHARSET_DIGITS;
502
- let str = '';
503
- for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
504
- return str;
505
- }
506
-
507
- /**
508
- * Random alpha-only string of `length` characters.
509
- *
510
- * @param {number} [length=8] - String length.
511
- * @param {object} [options] - Configuration options.
512
- * @param {boolean} [options.uppercase=false] - Use uppercase letters.
513
- * @param {boolean} [options.mixed=false] - Mix upper and lower.
514
- * @returns {string} An alphabetic string.
515
- */
516
- static alpha(length = 8, options = {}) {
517
- let pool;
518
- if (options.mixed) pool = CHARSET_LOWERCASE + CHARSET_UPPERCASE;
519
- else if (options.uppercase) pool = CHARSET_UPPERCASE;
520
- else pool = CHARSET_LOWERCASE;
521
- let str = '';
522
- for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
523
- return str;
524
- }
525
-
526
- /** Random boolean. */
527
- static boolean() { return rand() > 0.5; }
528
-
529
- // -- Dates --------------------------------------------------------------
530
-
531
- /**
532
- * Random Date between start and end.
533
- *
534
- * @param {Date} [start] - Start offset.
535
- * @param {Date} [end] - End value.
536
- * @returns {Date} A random Date object.
537
- */
538
- static date(start = new Date(2020, 0, 1), end = new Date()) {
539
- const ms = start.getTime() + rand() * (end.getTime() - start.getTime());
540
- return new Date(ms);
541
- }
542
-
543
- /**
544
- * Random ISO date string.
545
- *
546
- * @param {Date} [start] - Start offset.
547
- * @param {Date} [end] - End value.
548
- * @returns {string} An ISO 8601 date string.
549
- */
550
- static dateString(start, end) {
551
- return Fake.date(start, end).toISOString();
552
- }
553
-
554
- /**
555
- * Random date in the past.
556
- *
557
- * @param {object} [options] - Configuration options.
558
- * @param {number} [options.years=1] - How many years back the window spans.
559
- * @returns {Date} A Date in the past.
560
- */
561
- static datePast(options = {}) {
562
- const years = options.years || 1;
563
- const end = new Date();
564
- const start = new Date(end);
565
- start.setFullYear(start.getFullYear() - years);
566
- return Fake.date(start, end);
567
- }
568
-
569
- /**
570
- * Random date in the future.
571
- *
572
- * @param {object} [options] - Configuration options.
573
- * @param {number} [options.years=1] - Year range offset.
574
- * @returns {Date} A Date in the future.
575
- */
576
- static dateFuture(options = {}) {
577
- const years = options.years || 1;
578
- const start = new Date();
579
- const end = new Date(start);
580
- end.setFullYear(end.getFullYear() + years);
581
- return Fake.date(start, end);
582
- }
583
-
584
- // -- Text ---------------------------------------------------------------
585
-
586
- /**
587
- * Random word from the lorem ipsum vocabulary.
588
- *
589
- * @param {object} [options] - Configuration options.
590
- * @param {'lorem'|'hacker'|'adjective'|'noun'} [options.type='lorem'] - Content type variant.
591
- * @returns {string} A single word.
592
- */
593
- static word(options = {}) {
594
- const type = options.type || 'lorem';
595
- if (type === 'hacker') return _pick(HACKER_NOUNS);
596
- if (type === 'adjective') return _pick(ADJECTIVES);
597
- if (type === 'noun') return _pick(NOUNS);
598
- return _pick(LOREM_WORDS);
599
- }
600
-
601
- /**
602
- * Array of n random words.
603
- *
604
- * @param {number} [n=3] - Count.
605
- * @returns {string[]} Array of random words.
606
- */
607
- static words(n = 3) {
608
- return Array.from({ length: n }, () => Fake.word());
609
- }
610
-
611
- /**
612
- * Random sentence of `wordCount` words.
613
- *
614
- * @param {number|object} [wordCount] - Number of words.
615
- * @returns {string} A sentence ending with a period.
616
- */
617
- static sentence(wordCount) {
618
- const count = wordCount || _int(5, 15);
619
- const words = Array.from({ length: count }, () => _pick(LOREM_WORDS));
620
- words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
621
- return words.join(' ') + '.';
622
- }
623
-
624
- /**
625
- * Random paragraph of `sentences` sentences.
626
- *
627
- * @param {number} [sentences=3] - Number of sentences.
628
- * @returns {string} A multi-sentence paragraph.
629
- */
630
- static paragraph(sentences = 3) {
631
- return Array.from({ length: sentences }, () => Fake.sentence()).join(' ');
632
- }
633
-
634
- /**
635
- * Hacker-speak phrase ("synthesize redundant protocols").
636
- *
637
- * @returns {string} A hacker-speak phrase.
638
- */
639
- static hackerPhrase() {
640
- return `${_pick(HACKER_VERBS)} ${_pick(HACKER_ADJECTIVES)} ${_pick(HACKER_NOUNS)}`;
641
- }
642
-
643
- /**
644
- * URL-friendly slug from n random words.
645
- *
646
- * @param {number} [words=3] - Number of words.
647
- * @returns {string} e.g. 'forward-online-portal'
648
- */
649
- static slug(words = 3) {
650
- return Array.from({ length: words }, () => _pick(ADJECTIVES.concat(NOUNS)))
651
- .map(w => w.toLowerCase().replace(/[^a-z0-9]/g, '-'))
652
- .join('-');
653
- }
654
-
655
- /**
656
- * Random hashtag (no spaces, prefixed with #).
657
- *
658
- * @returns {string} e.g. '#resilientfuture'
659
- */
660
- static hashtag() {
661
- return '#' + _pick(ADJECTIVES) + _pick(NOUNS);
662
- }
663
-
664
- // -- Person -------------------------------------------------------------
665
-
666
- /**
667
- * Random job title.
668
- *
669
- * @param {object} [options] - Configuration options.
670
- * @param {boolean} [options.full=false] - Use a pre-built full title instead of generated.
671
- * @returns {string} A job title string.
672
- */
673
- static jobTitle(options = {}) {
674
- if (options.full) return _pick(JOB_TITLES);
675
- return `${_pick(JOB_DESCRIPTORS)} ${_pick(JOB_AREAS)} ${_pick(JOB_TYPES)}`;
676
- }
677
-
678
- /** Random job area / department string (e.g. 'Engineering'). */
679
- static jobArea() { return _pick(JOB_AREAS); }
680
-
681
- /** Random job type noun (e.g. 'Manager'). */
682
- static jobType() { return _pick(JOB_TYPES); }
683
-
684
- /** Random job level descriptor (e.g. 'Senior'). */
685
- static jobDescriptor() { return _pick(JOB_DESCRIPTORS); }
686
-
687
- /**
688
- * Random short biography string.
689
- *
690
- * @param {object} [options] - Configuration options.
691
- * @param {'phrase'|'full'} [options.style='phrase'] - Style variant.
692
- * @returns {string} A short biography phrase.
693
- *
694
- * @example
695
- * Fake.bio(); // 'avid maker | living one commit at a time'
696
- */
697
- static bio(options = {}) {
698
- if (options.style === 'full') {
699
- return `${_pick(BIO_ADJECTIVES)} ${_pick(BIO_NOUNS)} | ${_pick(BIO_PHRASES)}`;
700
- }
701
- return _pick(BIO_PHRASES);
702
- }
703
-
704
- /** Random zodiac sign. */
705
- static zodiacSign() { return _pick(ZODIAC_SIGNS); }
706
-
707
- /**
708
- * Random gender label.
709
- *
710
- * @param {object} [options] - Configuration options.
711
- * @param {boolean} [options.binary=false] - Return only 'Male' or 'Female'.
712
- * @returns {string} A gender label.
713
- */
714
- static gender(options = {}) {
715
- if (options.binary) return rand() > 0.5 ? 'Male' : 'Female';
716
- return _pick(GENDERS);
717
- }
718
-
719
- /** Random blood type (A+, B-, O+, AB+, …). */
720
- static bloodType() { return _pick(BLOOD_TYPES); }
721
-
722
- // -- Location ----------------------------------------------------------
723
-
724
- /**
725
- * Random city.
726
- *
727
- * @param {object} [options] - Configuration options.
728
- * @param {string} [options.country] - Filter by ISO 3166-1 alpha-2 country code.
729
- * @returns {string} A city name.
730
- */
731
- static city(options = {}) {
732
- const pool = options.country
733
- ? CITIES.filter(c => c.country === options.country.toUpperCase())
734
- : CITIES;
735
- return _pick(pool.length ? pool : CITIES).name;
736
- }
737
-
738
- /**
739
- * Random country.
740
- *
741
- * @param {object} [options] - Configuration options.
742
- * @param {boolean} [options.codeOnly=false] - Return ISO code only.
743
- * @param {boolean} [options.full=false] - Return { name, code } object.
744
- * @returns {string|{name:string,code:string}}
745
- */
746
- static country(options = {}) {
747
- const entry = _pick(COUNTRIES);
748
- if (options.codeOnly) return entry.code;
749
- if (options.full) return { ...entry };
750
- return entry.name;
751
- }
752
-
753
- /**
754
- * Random US state.
755
- *
756
- * @param {object} [options] - Configuration options.
757
- * @param {boolean} [options.abbr=false] - Return 2-letter abbreviation.
758
- * @param {boolean} [options.full=false] - Return { name, abbr } object.
759
- * @returns {string|{name:string,abbr:string}}
760
- */
761
- static state(options = {}) {
762
- const entry = _pick(US_STATES);
763
- if (options.abbr) return entry.abbr;
764
- if (options.full) return { ...entry };
765
- return entry.name;
766
- }
767
-
768
- /**
769
- * Random postal / ZIP code.
770
- *
771
- * @param {object} [options] - Configuration options.
772
- * @param {string} [options.countryCode='US'] - ISO country code.
773
- * @returns {string} A postal code string.
774
- */
775
- static zipCode(options = {}) {
776
- const code = (options.countryCode || 'US').toUpperCase();
777
- const pattern = ZIP_PATTERNS[code] || ZIP_PATTERNS['US'];
778
- return _expandTemplate(pattern);
779
- }
780
-
781
- /**
782
- * Random latitude as a float in [-90, 90].
783
- *
784
- * @param {object} [options] - Configuration options.
785
- * @param {number} [options.min=-90] - Minimum value.
786
- * @param {number} [options.max=90] - Maximum value.
787
- * @param {number} [options.decimals=6] - Decimal places.
788
- * @returns {number} A latitude value.
789
- */
790
- static latitude(options = {}) {
791
- return Fake.float(options.min ?? -90, options.max ?? 90, options.decimals ?? 6);
792
- }
793
-
794
- /**
795
- * Random longitude as a float in [-180, 180].
796
- *
797
- * @param {object} [options] - Configuration options.
798
- * @param {number} [options.min=-180] - Minimum value.
799
- * @param {number} [options.max=180] - Maximum value.
800
- * @param {number} [options.decimals=6] - Decimal places.
801
- * @returns {number} A longitude value.
802
- */
803
- static longitude(options = {}) {
804
- return Fake.float(options.min ?? -180, options.max ?? 180, options.decimals ?? 6);
805
- }
806
-
807
- /**
808
- * Random { latitude, longitude } coordinate object.
809
- *
810
- * @param {object} [options] - Passed to latitude() and longitude().
811
- * @returns {{ latitude: number, longitude: number }}
812
- */
813
- static coordinates(options = {}) {
814
- return { latitude: Fake.latitude(options), longitude: Fake.longitude(options) };
815
- }
816
-
817
- /** Random IANA timezone identifier (e.g. 'America/New_York'). */
818
- static timezone() { return _pick(TIMEZONES); }
819
-
820
- /**
821
- * Random street name (e.g. 'Oak Avenue').
822
- *
823
- * @returns {string} A street name.
824
- */
825
- static streetName() {
826
- return `${_pick(STREET_NAMES)} ${_pick(STREET_TYPES)}`;
827
- }
828
-
829
- /**
830
- * Random full street address.
831
- *
832
- * @param {object} [options] - Configuration options.
833
- * @param {string} [options.countryCode='US'] - ISO country code.
834
- * @param {'string'|'object'} [options.format='string'] - Output format.
835
- * @returns {string|object} A formatted address string, or an address object when format is 'object'.
836
- *
837
- * @example
838
- * Fake.address();
839
- * // '742 Evergreen Terrace, Springfield, IL 62701'
840
- *
841
- * Fake.address({ format: 'object' });
842
- * // { streetNumber, streetName, city, state, zipCode, country }
843
- */
844
- static address(options = {}) {
845
- const code = (options.countryCode || 'US').toUpperCase();
846
- const streetNum = _int(1, 9999);
847
- const street = Fake.streetName();
848
- const city = Fake.city({ country: code });
849
- const zipCode = Fake.zipCode({ countryCode: code });
850
- const stateEntry = code === 'US' ? _pick(US_STATES) : null;
851
- const countryName = (COUNTRIES.find(c => c.code === code) || { name: code }).name;
852
-
853
- if (options.format === 'object') {
854
- return {
855
- streetNumber: String(streetNum),
856
- streetName: street,
857
- city,
858
- state: stateEntry ? stateEntry.abbr : null,
859
- zipCode,
860
- country: countryName,
861
- };
862
- }
863
-
864
- if (stateEntry) {
865
- return `${streetNum} ${street}, ${city}, ${stateEntry.abbr} ${zipCode}`;
866
- }
867
- return `${streetNum} ${street}, ${city} ${zipCode}, ${countryName}`;
868
- }
869
-
870
- // -- Commerce -----------------------------------------------------------
871
-
872
- /**
873
- * Random product name (adjective + material + noun).
874
- *
875
- * @param {object} [options] - Configuration options.
876
- * @param {boolean} [options.withMaterial=true] - Include a material word.
877
- * @returns {string} A product name.
878
- */
879
- static productName(options = {}) {
880
- const withMaterial = options.withMaterial !== false;
881
- if (withMaterial) {
882
- return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_MATERIALS)} ${_pick(PRODUCT_NOUNS)}`;
883
- }
884
- return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_NOUNS)}`;
885
- }
886
-
887
- /** Random product category (e.g. 'Electronics'). */
888
- static category() { return _pick(CATEGORIES); }
889
-
890
- /** Random business department name. */
891
- static department() { return _pick(DEPARTMENTS); }
892
-
893
- /**
894
- * Random company name.
895
- *
896
- * @param {object} [options] - Configuration options.
897
- * @param {boolean} [options.suffix=true] - Append LLC / Inc. / etc.
898
- * @returns {string} A company name.
899
- */
900
- static company(options = {}) {
901
- const withSuffix = options.suffix !== false;
902
- const name = `${_pick(COMPANY_ADJECTIVES)} ${_pick(COMPANY_NOUNS)}`;
903
- return withSuffix ? `${name} ${_pick(COMPANY_SUFFIXES)}` : name;
904
- }
905
-
906
- /**
907
- * Random price as a float with 2 decimal places.
908
- *
909
- * @param {object} [options] - Configuration options.
910
- * @param {number} [options.min=0.99] - Minimum value.
911
- * @param {number} [options.max=999.99] - Maximum value.
912
- * @param {string} [options.symbol=''] - Prepend a currency symbol.
913
- * @returns {string|number} - String when symbol is provided, number otherwise.
914
- */
915
- static price(options = {}) {
916
- const val = Fake.float(options.min ?? 0.99, options.max ?? 999.99, 2);
917
- return options.symbol ? `${options.symbol}${val.toFixed(2)}` : val;
918
- }
919
-
920
- /** Random industry sector name. */
921
- static industry() { return _pick(INDUSTRIES); }
922
-
923
- /** Random catch phrase buzzword phrase. */
924
- static catchPhrase() {
925
- return `${_pick(CATCH_PHRASE_ADJECTIVES)} ${_pick(CATCH_PHRASE_NOUNS)}`;
926
- }
927
-
928
- // -- Internet & Network -------------------------------------------------
929
-
930
- /**
931
- * Random email address (backward-compatible shorthand — same as email()).
932
- * Accepts no arguments for historical use.
933
- */
934
-
935
- /** Random UUID v4. */
936
- static uuid() { return _uuid(); }
937
-
938
- /**
939
- * Random domain name (adjective-noun.tld).
940
- *
941
- * @param {object} [options] - Configuration options.
942
- * @param {string} [options.tld] - Force a specific TLD.
943
- * @returns {string} A domain name.
944
- */
945
- static domainName(options = {}) {
946
- const tld = options.tld || _pick(TLDS);
947
- return `${_pick(DOMAIN_ADJECTIVES)}-${_pick(DOMAIN_NOUNS)}.${tld}`;
948
- }
949
-
950
- /**
951
- * Random URL.
952
- *
953
- * @param {object} [options] - Configuration options.
954
- * @param {'http'|'https'} [options.protocol='https'] - URL protocol.
955
- * @param {boolean} [options.appendSlash=false] - Append trailing slash.
956
- * @returns {string} A URL string.
957
- */
958
- static url(options = {}) {
959
- const proto = options.protocol || 'https';
960
- const path = options.noPath ? '' : `/${Fake.word()}`;
961
- const slash = options.appendSlash ? '/' : '';
962
- return `${proto}://${Fake.domainName()}${path}${slash}`;
963
- }
964
-
965
- /**
966
- * Random IPv4 address.
967
- *
968
- * @param {object} [options] - Configuration options.
969
- * @param {'any'|'private-a'|'private-b'|'private-c'|'loopback'} [options.network='any'] - Network type.
970
- * @returns {string} An IPv4 address.
971
- */
972
- static ip(options = {}) {
973
- const net = options.network || 'any';
974
- if (net === 'loopback') return `127.0.0.${_int(1, 254)}`;
975
- if (net === 'private-a') return `10.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
976
- if (net === 'private-b') return `172.${_int(16,31)}.${_int(0,255)}.${_int(1,254)}`;
977
- if (net === 'private-c') return `192.168.${_int(0,255)}.${_int(1,254)}`;
978
- return `${_int(1,254)}.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
979
- }
980
-
981
- /**
982
- * Random IPv6 address.
983
- *
984
- * @returns {string} An IPv6 address.
985
- */
986
- static ipv6() {
987
- const groups = Array.from({ length: 8 }, () =>
988
- _int(0, 65535).toString(16).padStart(4, '0')
989
- );
990
- return groups.join(':');
991
- }
992
-
993
- /**
994
- * Random MAC address.
995
- *
996
- * @param {object} [options] - Configuration options.
997
- * @param {':'|'-'|''} [options.separator=':'] - Separator character.
998
- * @param {boolean} [options.realisticOUI=false] - Use a real vendor OUI prefix.
999
- * @returns {string} A MAC address string.
1000
- */
1001
- static mac(options = {}) {
1002
- const sep = options.separator ?? ':';
1003
- if (options.realisticOUI) {
1004
- const oui = _pick(MAC_OUIS).replace(/:/g, sep);
1005
- const rest = Array.from({ length: 3 }, () =>
1006
- _int(0, 255).toString(16).padStart(2, '0')
1007
- ).join(sep);
1008
- return `${oui}${sep}${rest}`;
1009
- }
1010
- return Array.from({ length: 6 }, () =>
1011
- _int(0, 255).toString(16).padStart(2, '0')
1012
- ).join(sep);
1013
- }
1014
-
1015
- /**
1016
- * Random network port number.
1017
- *
1018
- * @param {object} [options] - Configuration options.
1019
- * @param {'well-known'|'registered'|'dynamic'|'any'} [options.range='any'] - Address range.
1020
- * well-known → 1–1023
1021
- * registered → 1024–49151
1022
- * dynamic → 49152–65535
1023
- * any → 1–65535
1024
- * @returns {number} A port number.
1025
- */
1026
- static port(options = {}) {
1027
- const range = options.range || 'any';
1028
- if (range === 'well-known') return _int(1, 1023);
1029
- if (range === 'registered') return _int(1024, 49151);
1030
- if (range === 'dynamic') return _int(49152, 65535);
1031
- return _int(1, 65535);
1032
- }
1033
-
1034
- /**
1035
- * Random HTTP method.
1036
- *
1037
- * @param {object} [options] - Configuration options.
1038
- * @param {string[]} [options.methods] - Restrict to specific methods.
1039
- * @returns {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'} An HTTP method string.
1040
- */
1041
- static httpMethod(options = {}) {
1042
- const pool = options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
1043
- return _pick(pool);
1044
- }
1045
-
1046
- /**
1047
- * Random user agent string (realistic browser/client).
1048
- *
1049
- * @returns {string} A user agent string.
1050
- */
1051
- static userAgent() { return _pick(USER_AGENTS); }
1052
-
1053
- /**
1054
- * Random password-like string. NOT suitable for real passwords — uses a
1055
- * PRNG seeded from Math.random, not a CSPRNG.
1056
- *
1057
- * @param {object} [options] - Configuration options.
1058
- * @param {number} [options.length=16] - Character count.
1059
- * @param {boolean} [options.uppercase=true] - Include uppercase letters.
1060
- * @param {boolean} [options.lowercase=true] - Include lowercase letters.
1061
- * @param {boolean} [options.digits=true] - Include digits.
1062
- * @param {boolean} [options.special=false] - Include special characters.
1063
- * @param {string} [options.prefix=''] - Prepend a prefix.
1064
- * @returns {string} A password string.
1065
- */
1066
- static password(options = {}) {
1067
- const len = options.length ?? 16;
1068
- const prefix = options.prefix ?? '';
1069
- let pool = '';
1070
- if (options.lowercase !== false) pool += CHARSET_LOWERCASE;
1071
- if (options.uppercase !== false) pool += CHARSET_UPPERCASE;
1072
- if (options.digits !== false) pool += CHARSET_DIGITS;
1073
- if (options.special === true) pool += CHARSET_SPECIAL;
1074
- pool = pool || (CHARSET_LOWERCASE + CHARSET_UPPERCASE + CHARSET_DIGITS);
1075
- const body = Array.from({ length: Math.max(0, len - prefix.length) }, () =>
1076
- pool[Math.floor(rand() * pool.length)]
1077
- ).join('');
1078
- return `${prefix}${body}`;
1079
- }
1080
-
1081
- // -- Colors -------------------------------------------------------------
1082
-
1083
- /**
1084
- * Random hex color code.
1085
- *
1086
- * @returns {string} e.g. '#3a7f2b'
1087
- */
1088
- static color() {
1089
- return '#' + Math.floor(rand() * 16777215).toString(16).padStart(6, '0');
1090
- }
1091
-
1092
- /**
1093
- * Random RGB color object or string.
1094
- *
1095
- * @param {object} [options] - Configuration options.
1096
- * @param {'object'|'css'} [options.format='css'] - Output format.
1097
- * @returns {{ r:number, g:number, b:number }|string}
1098
- */
1099
- static rgb(options = {}) {
1100
- const r = _int(0, 255), g = _int(0, 255), b = _int(0, 255);
1101
- if (options.format === 'object') return { r, g, b };
1102
- return `rgb(${r}, ${g}, ${b})`;
1103
- }
1104
-
1105
- /**
1106
- * Random HSL color string or object.
1107
- *
1108
- * @param {object} [options] - Configuration options.
1109
- * @param {'object'|'css'} [options.format='css'] - Output format.
1110
- * @returns {{ h:number, s:number, l:number }|string}
1111
- */
1112
- static hsl(options = {}) {
1113
- const h = _int(0, 360), s = _int(20, 100), l = _int(20, 80);
1114
- if (options.format === 'object') return { h, s, l };
1115
- return `hsl(${h}, ${s}%, ${l}%)`;
1116
- }
1117
-
1118
- // -- Helpers ------------------------------------------------------------
1119
-
1120
- /**
1121
- * Pick a random element from an array.
1122
- *
1123
- * @template T
1124
- * @param {T[]} arr - Input array.
1125
- * @returns {T} A random element from the array.
1126
- */
1127
- static pick(arr) { return _pick(arr); }
1128
-
1129
- /**
1130
- * Pick n random elements from an array (no duplicates).
1131
- *
1132
- * @template T
1133
- * @param {T[]} arr - Input array.
1134
- * @param {number} n - Count.
1135
- * @returns {T[]} An array of n distinct random elements.
1136
- */
1137
- static pickMany(arr, n) { return _pickMany(arr, n); }
1138
-
1139
- /**
1140
- * Shuffle an array in-place using Fisher-Yates and return it.
1141
- *
1142
- * @template T
1143
- * @param {T[]} arr - Input array.
1144
- * @returns {T[]} The shuffled array (mutated in-place).
1145
- */
1146
- static shuffle(arr) {
1147
- for (let i = arr.length - 1; i > 0; i--) {
1148
- const j = Math.floor(rand() * (i + 1));
1149
- [arr[i], arr[j]] = [arr[j], arr[i]];
1150
- }
1151
- return arr;
1152
- }
1153
-
1154
- /**
1155
- * Random JSON-safe object (useful as a quick fixture value).
1156
- *
1157
- * @returns {{ key: string, value: string, count: number, active: boolean }}
1158
- */
1159
- static json() {
1160
- return {
1161
- key: Fake.word(),
1162
- value: Fake.sentence(3),
1163
- count: _int(1, 100),
1164
- active: Fake.boolean(),
1165
- };
1166
- }
1167
-
1168
- /**
1169
- * Random element from an enum-like array, validated at call time.
1170
- *
1171
- * @template T
1172
- * @param {readonly T[]} values - Array of values.
1173
- * @returns {T} A random element from the values array.
1174
- */
1175
- static enumValue(values) {
1176
- if (!Array.isArray(values) || values.length === 0) {
1177
- throw new Error('Fake.enumValue: requires a non-empty array');
1178
- }
1179
- return _pick(values);
1180
- }
1181
- }
1182
-
1183
- // Static uniqueness tracker — shared across all calls in the process lifetime
1184
- Fake._tracker = new UniqueTracker();
1185
-
1186
- module.exports = { Fake };
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module seed/fake
5
+ * @description Extensible fake data generator.
6
+ *
7
+ * Key capabilities:
8
+ * • Seeded / reproducible output — Fake.seed(42)
9
+ * • Guaranteed-unique values — Fake.unique(() => Fake.email())
10
+ * • Multi-locale names — Fake.firstName({ locale: 'ja', sex: 'female' })
11
+ * • Rich phone formats — Fake.phone({ countryCode: 'DE', format: 'international' })
12
+ * • Configurable emails — Fake.email({ provider: 'company.io' })
13
+ * • Flexible usernames — Fake.username({ style: 'underscore' })
14
+ * • Fixed-length numeric strings — Fake.numericString(8)
15
+ * • Person: job titles, bio, zodiac, gender, prefix/suffix
16
+ * • Location: city, country, state, address, lat/lng, timezone
17
+ * • Commerce: product, company, category, price, industry
18
+ * • Internet: password, mac, port, userAgent, domain, ipv6, slug
19
+ * • All original generators retained (100 % backward-compatible)
20
+ */
21
+
22
+ const crypto = require('crypto');
23
+ const { rand, seed: _seedFn, getSeed } = require('./rng');
24
+ const { UniqueTracker } = require('./unique');
25
+
26
+ // -- Data modules ------------------------------------------------
27
+ const { NAMES, LOCALES } = require('./data/names');
28
+ const { PHONE_BY_COUNTRY, COUNTRY_CODES } = require('./data/phone');
29
+ const {
30
+ EMAIL_PROVIDERS, SAFE_DOMAINS, TLDS,
31
+ DOMAIN_ADJECTIVES, DOMAIN_NOUNS,
32
+ USERNAME_SEPARATORS, USER_AGENTS, MAC_OUIS,
33
+ CHARSET_LOWERCASE, CHARSET_UPPERCASE, CHARSET_DIGITS, CHARSET_SPECIAL,
34
+ } = require('./data/internet');
35
+ const {
36
+ LOREM_WORDS, HACKER_ADJECTIVES, HACKER_NOUNS, HACKER_VERBS,
37
+ ADJECTIVES, NOUNS,
38
+ } = require('./data/words');
39
+ const {
40
+ NAME_PREFIXES, NAME_SUFFIXES,
41
+ JOB_DESCRIPTORS, JOB_AREAS, JOB_TYPES, JOB_TITLES,
42
+ ZODIAC_SIGNS, GENDERS, BIO_ADJECTIVES, BIO_NOUNS, BIO_PHRASES,
43
+ BLOOD_TYPES,
44
+ } = require('./data/person');
45
+ const {
46
+ CITIES, COUNTRIES, US_STATES,
47
+ STREET_TYPES, STREET_NAMES, ZIP_PATTERNS, TIMEZONES,
48
+ } = require('./data/locations');
49
+ const {
50
+ PRODUCT_ADJECTIVES, PRODUCT_MATERIALS, PRODUCT_NOUNS,
51
+ CATEGORIES, DEPARTMENTS,
52
+ COMPANY_ADJECTIVES, COMPANY_NOUNS, COMPANY_SUFFIXES, INDUSTRIES,
53
+ CATCH_PHRASE_ADJECTIVES, CATCH_PHRASE_NOUNS,
54
+ } = require('./data/commerce');
55
+
56
+ // ================================================================
57
+ // Internal helpers
58
+ // ================================================================
59
+
60
+ /** @private Pick a random element from an array using the active RNG. */
61
+ function _pick(arr) {
62
+ return arr[Math.floor(rand() * arr.length)];
63
+ }
64
+
65
+ /** @private Pick n unique-indexed elements from an array (no repeats). */
66
+ function _pickMany(arr, n) {
67
+ const copy = arr.slice();
68
+ for (let i = copy.length - 1; i > 0; i--) {
69
+ const j = Math.floor(rand() * (i + 1));
70
+ [copy[i], copy[j]] = [copy[j], copy[i]];
71
+ }
72
+ return copy.slice(0, Math.min(n, arr.length));
73
+ }
74
+
75
+ /** @private Random integer in [min, max] inclusive. */
76
+ function _int(min, max) {
77
+ return Math.floor(rand() * (max - min + 1)) + min;
78
+ }
79
+
80
+ /**
81
+ * @private
82
+ * Expand a phone / zip template string.
83
+ * # → digit 0–9 N → digit 1–9 A → uppercase letter
84
+ */
85
+ function _expandTemplate(template) {
86
+ return template.replace(/[#NA]/g, c => {
87
+ if (c === '#') return _int(0, 9);
88
+ if (c === 'N') return _int(1, 9);
89
+ return String.fromCharCode(_int(65, 90)); // c === 'A'
90
+ });
91
+ }
92
+
93
+ /** @private Resolve name pool for a locale, falling back to 'en'. */
94
+ function _namePool(locale) {
95
+ return NAMES[locale] || NAMES.en;
96
+ }
97
+
98
+ /** @private Derive sex ('male'|'female') from options or random. */
99
+ function _resolveSex(options = {}) {
100
+ const { sex } = options;
101
+ if (sex === 'male' || sex === 'female') return sex;
102
+ return rand() > 0.5 ? 'male' : 'female';
103
+ }
104
+
105
+ /** @private UUID v4 fallback for environments without crypto.randomUUID. */
106
+ function _uuid() {
107
+ if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
108
+ const b = crypto.randomBytes(16);
109
+ b[6] = (b[6] & 0x0f) | 0x40;
110
+ b[8] = (b[8] & 0x3f) | 0x80;
111
+ const h = b.toString('hex');
112
+ return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
113
+ }
114
+
115
+ // ================================================================
116
+ // Fake class
117
+ // ================================================================
118
+
119
+ /**
120
+ * Static fake data generator with seeded RNG, locale support, and
121
+ * built-in uniqueness guarantees.
122
+ *
123
+ * All methods are **100 % backward compatible** with the original Fake class.
124
+ *
125
+ * @example
126
+ * // Reproducible output
127
+ * Fake.seed(42);
128
+ * Fake.firstName(); // always the same value
129
+ *
130
+ * // Guaranteed unique emails
131
+ * const emails = Array.from({ length: 200 }, () =>
132
+ * Fake.unique(() => Fake.email())
133
+ * );
134
+ *
135
+ * // Locale-specific names
136
+ * Fake.firstName({ locale: 'ja', sex: 'female' }); // e.g. 'Hina'
137
+ */
138
+ class Fake
139
+ {
140
+ // -- Seeding & Uniqueness ------------------------------------------------
141
+
142
+ /**
143
+ * Set a deterministic seed so all subsequent calls produce the same output.
144
+ * Pass `null` or `undefined` to reset to Math.random.
145
+ *
146
+ * @param {number|string|null} [value] - Value to set.
147
+ * @returns {number|null} The resolved numeric seed.
148
+ *
149
+ * @example
150
+ * Fake.seed(42);
151
+ * Fake.firstName(); // reproducible
152
+ * Fake.seed(null); // back to random
153
+ */
154
+ static seed(value) { return _seedFn(value); }
155
+
156
+ /** @returns {number|null} Active seed, or null if using Math.random. */
157
+ static getSeed() { return getSeed(); }
158
+
159
+ /**
160
+ * Generate a guaranteed-unique value within a namespace.
161
+ *
162
+ * @param {() => any} fn - Generator function.
163
+ * @param {object} [options] - Configuration options.
164
+ * @param {string} [options.key] - Namespace key (defaults to function name).
165
+ * @param {number} [options.maxAttempts] - Retry limit (default 1000).
166
+ * @returns {any} The unique generated value.
167
+ *
168
+ * @example
169
+ * const email = Fake.unique(() => Fake.email(), { key: 'email' });
170
+ */
171
+ static unique(fn, options = {}) {
172
+ const key = options.key || fn.name || 'default';
173
+ return Fake._tracker.generate(key, fn, options.maxAttempts);
174
+ }
175
+
176
+ /**
177
+ * Reset unique-value tracking.
178
+ * @param {string} [key] - Clear only this namespace, or all if omitted.
179
+ */
180
+ static resetUnique(key) { Fake._tracker.reset(key); }
181
+
182
+ /**
183
+ * How many unique values have been generated for a namespace.
184
+ * @param {string} key - Cache or storage key.
185
+ * @returns {number} Count of unique values tracked for the key.
186
+ */
187
+ static uniqueCount(key) { return Fake._tracker.seen(key); }
188
+
189
+ // -- Names --------------------------------------------------------------
190
+
191
+ /**
192
+ * Random first name.
193
+ *
194
+ * @param {object} [options] - Configuration options.
195
+ * @param {'male'|'female'} [options.sex] - Constrain to a sex.
196
+ * @param {string} [options.locale='en'] - Locale code (en, es, fr, de, …).
197
+ * @param {boolean} [options.unique=false] - Guarantee uniqueness per locale+sex.
198
+ * @returns {string} A random first name.
199
+ */
200
+ static firstName(options = {}) {
201
+ const locale = options.locale || 'en';
202
+ const sex = _resolveSex(options);
203
+ const pool = _namePool(locale);
204
+ const fn = () => _pick(pool[sex]);
205
+ if (options.unique) {
206
+ return Fake.unique(fn, { key: `firstName_${locale}_${sex}` });
207
+ }
208
+ return fn();
209
+ }
210
+
211
+ /**
212
+ * Random last name.
213
+ *
214
+ * @param {object} [options] - Configuration options.
215
+ * @param {string} [options.locale='en'] - Locale code (e.g. "en").
216
+ * @param {boolean} [options.unique=false] - Whether to ensure unique values.
217
+ * @returns {string} A random last name.
218
+ */
219
+ static lastName(options = {}) {
220
+ const locale = options.locale || 'en';
221
+ const pool = _namePool(locale);
222
+ const fn = () => _pick(pool.last);
223
+ if (options.unique) return Fake.unique(fn, { key: `lastName_${locale}` });
224
+ return fn();
225
+ }
226
+
227
+ /**
228
+ * Random middle name (falls back to first-name pool if locale lacks one).
229
+ *
230
+ * @param {object} [options] - Configuration options.
231
+ * @param {'male'|'female'} [options.sex] - Biological sex (male/female).
232
+ * @param {string} [options.locale='en'] - Locale code (e.g. "en").
233
+ * @returns {string} A random middle name.
234
+ */
235
+ static middleName(options = {}) {
236
+ // Reuse first-name pool since most locales share it
237
+ return Fake.firstName({ sex: options.sex, locale: options.locale });
238
+ }
239
+
240
+ /**
241
+ * Random full name.
242
+ *
243
+ * @param {object} [options] - Configuration options.
244
+ * @param {'male'|'female'} [options.sex] - Biological sex (male/female).
245
+ * @param {string} [options.locale='en'] - Locale code (e.g. "en").
246
+ * @param {boolean} [options.prefix=false] - Include name prefix (Mr., Dr., …).
247
+ * @param {boolean} [options.middle=false] - Include a middle name.
248
+ * @param {boolean} [options.suffix=false] - Include credential suffix (PhD, Jr., …).
249
+ * @param {string} [options.firstName] - Override the first name.
250
+ * @param {string} [options.lastName] - Override the last name.
251
+ * @param {boolean} [options.unique=false] - Guarantee uniqueness per locale.
252
+ * @returns {string} A full name string.
253
+ */
254
+ static fullName(options = {}) {
255
+ const locale = options.locale || 'en';
256
+ const sex = _resolveSex(options);
257
+
258
+ const fn = () => {
259
+ const parts = [];
260
+ if (options.prefix) parts.push(Fake.namePrefix({ sex }));
261
+ parts.push(options.firstName || Fake.firstName({ sex, locale }));
262
+ if (options.middle) parts.push(Fake.middleName({ sex, locale }));
263
+ parts.push(options.lastName || Fake.lastName({ locale }));
264
+ if (options.suffix) parts.push(Fake.nameSuffix());
265
+ return parts.join(' ');
266
+ };
267
+
268
+ if (options.unique) return Fake.unique(fn, { key: `fullName_${locale}` });
269
+ return fn();
270
+ }
271
+
272
+ /**
273
+ * Name prefix (Mr., Ms., Dr., Prof., …).
274
+ *
275
+ * @param {object} [options] - Configuration options.
276
+ * @param {'male'|'female'|'neutral'} [options.sex] - Biological sex (male/female).
277
+ * @returns {string} A title prefix like 'Mr.' or 'Dr.'.
278
+ */
279
+ static namePrefix(options = {}) {
280
+ const sex = options.sex === 'male' ? 'male'
281
+ : options.sex === 'female' ? 'female'
282
+ : options.sex === 'neutral' ? 'neutral'
283
+ : rand() > 0.5 ? 'male'
284
+ : 'female';
285
+ return _pick(NAME_PREFIXES[sex]);
286
+ }
287
+
288
+ /** Random name suffix (Jr., Sr., PhD, MD, …). */
289
+ static nameSuffix() { return _pick(NAME_SUFFIXES); }
290
+
291
+ /** All supported locale codes. */
292
+ static locales() { return LOCALES.slice(); }
293
+
294
+ // -- Phone Numbers ------------------------------------------------------
295
+
296
+ /**
297
+ * Random phone number.
298
+ *
299
+ * @param {object} [options] - Configuration options.
300
+ * @param {string} [options.countryCode] - ISO 3166-1 alpha-2 (e.g. 'US', 'DE').
301
+ * Defaults to a random country.
302
+ * @param {'human'|'national'|'international'} [options.format='human'] - Output format.
303
+ * @param {boolean} [options.unique=false] - Whether to ensure unique values.
304
+ * @returns {string} Formatted string.
305
+ *
306
+ * @example
307
+ * Fake.phone(); // '(555) 123-4567'
308
+ * Fake.phone({ countryCode: 'DE' }); // '0174 #######'
309
+ * Fake.phone({ format: 'international' }); // '+1 555 123 4567'
310
+ */
311
+ static phone(options = {}) {
312
+ const code = (options.countryCode || 'US').toUpperCase();
313
+ const country = PHONE_BY_COUNTRY[code] || PHONE_BY_COUNTRY['US'];
314
+ // Default to 'national' so bare Fake.phone() produces the classic
315
+ // (###) ###-#### format and remains backward compatible.
316
+ const style = options.format || 'national';
317
+ const formats = country.formats[style] || country.formats.national;
318
+ const fn = () => _expandTemplate(_pick(formats));
319
+ if (options.unique) return Fake.unique(fn, { key: `phone_${code}_${style}` });
320
+ return fn();
321
+ }
322
+
323
+ /**
324
+ * All supported phone country codes.
325
+ * @returns {string[]} Array of ISO 3166-1 alpha-2 codes.
326
+ */
327
+ static phoneCodes() { return COUNTRY_CODES.slice(); }
328
+
329
+ // -- Email --------------------------------------------------------------
330
+
331
+ /**
332
+ * Random email address.
333
+ *
334
+ * @param {object} [options] - Configuration options.
335
+ * @param {string} [options.firstName] - Override the local-part first name.
336
+ * @param {string} [options.lastName] - Override the local-part last name.
337
+ * @param {string} [options.provider] - Force a provider domain.
338
+ * @param {boolean} [options.safe=false] - Use only example./test. safe domains.
339
+ * @param {string} [options.locale='en'] - Locale for random name generation.
340
+ * @param {boolean} [options.unique=false] - Whether to ensure unique values.
341
+ * @returns {string} An email address string.
342
+ *
343
+ * @example
344
+ * Fake.email();
345
+ * Fake.email({ provider: 'company.io', unique: true });
346
+ * Fake.email({ safe: true }); // only example.com/test.com domains
347
+ */
348
+ static email(options = {}) {
349
+ const locale = options.locale || 'en';
350
+ const fn = () => {
351
+ const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
352
+ .replace(/[^a-z0-9]/g, '');
353
+ const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
354
+ .replace(/[^a-z0-9]/g, '');
355
+ const num = _int(1, 999);
356
+ const domain = options.provider
357
+ ? options.provider
358
+ : options.safe
359
+ ? _pick(SAFE_DOMAINS)
360
+ : _pick(EMAIL_PROVIDERS);
361
+
362
+ const patterns = [
363
+ `${first}.${last}${num}@${domain}`,
364
+ `${first}${last}@${domain}`,
365
+ `${first}_${last}@${domain}`,
366
+ `${first}${num}@${domain}`,
367
+ `${first[0]}${last}@${domain}`,
368
+ ];
369
+ return _pick(patterns);
370
+ };
371
+
372
+ if (options.unique) return Fake.unique(fn, { key: `email_${options.provider || 'any'}` });
373
+ return fn();
374
+ }
375
+
376
+ // -- Username -----------------------------------------------------------
377
+
378
+ /**
379
+ * Random username.
380
+ *
381
+ * @param {object} [options] - Configuration options.
382
+ * @param {string} [options.firstName] - Override first name component.
383
+ * @param {string} [options.lastName] - Override last name component.
384
+ * @param {'dot'|'underscore'|'none'|'random'} [options.style='random'] - Style variant.
385
+ * @param {boolean} [options.numbers=true] - Append a random number suffix.
386
+ * @param {string} [options.locale='en'] - Locale code (e.g. "en").
387
+ * @param {boolean} [options.unique=false] - Whether to ensure unique values.
388
+ * @returns {string} A username string.
389
+ *
390
+ * @example
391
+ * Fake.username(); // 'alice_smith42'
392
+ * Fake.username({ style: 'dot' }); // 'john.doe91'
393
+ * Fake.username({ style: 'none', numbers: false }); // 'janedoe'
394
+ */
395
+ static username(options = {}) {
396
+ const locale = options.locale || 'en';
397
+ const style = options.style || 'random';
398
+ const useNums = options.numbers !== false;
399
+
400
+ const fn = () => {
401
+ const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
402
+ .replace(/[^a-z0-9]/g, '');
403
+ const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
404
+ .replace(/[^a-z0-9]/g, '');
405
+
406
+ let sep;
407
+ if (style === 'dot') sep = '.';
408
+ else if (style === 'underscore') sep = '_';
409
+ else if (style === 'none') sep = '';
410
+ else sep = _pick(USERNAME_SEPARATORS);
411
+
412
+ const base = `${first}${sep}${last}`;
413
+ const num = useNums ? _int(1, 9999) : '';
414
+ return `${base}${num}`;
415
+ };
416
+
417
+ if (options.unique) return Fake.unique(fn, { key: `username_${style}` });
418
+ return fn();
419
+ }
420
+
421
+ // -- Numbers ------------------------------------------------------------
422
+
423
+ /**
424
+ * Random integer in [min, max].
425
+ *
426
+ * @param {number|object} [min=0] - Min bound, or options object.
427
+ * @param {number} [max=100] - Maximum value.
428
+ * @returns {number} A random integer.
429
+ */
430
+ static integer(min = 0, max = 100) {
431
+ if (typeof min === 'object' && min !== null) {
432
+ const opts = min;
433
+ return _int(opts.min ?? 0, opts.max ?? 100);
434
+ }
435
+ return _int(Math.floor(min), Math.floor(max));
436
+ }
437
+
438
+ /**
439
+ * Random float in [min, max].
440
+ *
441
+ * @param {number|object} [min=0] - Minimum value.
442
+ * @param {number} [max=100] - Maximum value.
443
+ * @param {number} [decimals=2] - Decimal places.
444
+ * @returns {number} A random float.
445
+ */
446
+ static float(min = 0, max = 100, decimals = 2) {
447
+ if (typeof min === 'object' && min !== null) {
448
+ const opts = min;
449
+ return Number((rand() * ((opts.max ?? 100) - (opts.min ?? 0)) + (opts.min ?? 0))
450
+ .toFixed(opts.decimals ?? 2));
451
+ }
452
+ return Number((rand() * (max - min) + min).toFixed(decimals));
453
+ }
454
+
455
+ /**
456
+ * Random numeric string with an exact number of digits.
457
+ * Leading zeros are preserved (unlike integer()).
458
+ *
459
+ * @param {number} [length=6] - String length.
460
+ * @param {object} [options] - Configuration options.
461
+ * @param {boolean} [options.leadingZeros=true] - Allow a leading zero.
462
+ * @param {string} [options.separator=''] - Insert separator every N digits.
463
+ * @param {number} [options.groupSize=3] - Group size when using separator.
464
+ * @returns {string} A fixed-length digit string.
465
+ *
466
+ * @example
467
+ * Fake.numericString(6); // '047283'
468
+ * Fake.numericString(16, { separator: '-', groupSize: 4 }); // '1234-5678-9012-3456'
469
+ */
470
+ static numericString(length = 6, options = {}) {
471
+ const allowLeading = options.leadingZeros !== false;
472
+ let str = '';
473
+ for (let i = 0; i < length; i++) {
474
+ const min = (!allowLeading && i === 0) ? 1 : 0;
475
+ str += _int(min, 9);
476
+ }
477
+ if (options.separator) {
478
+ const gs = options.groupSize || 3;
479
+ const re = new RegExp(`(.{${gs}})(?=.)`, 'g');
480
+ str = str.replace(re, `$1${options.separator}`);
481
+ }
482
+ return str;
483
+ }
484
+
485
+ /**
486
+ * Random alphanumeric string of `length` characters.
487
+ *
488
+ * @param {number} [length=10] - String length.
489
+ * @param {object} [options] - Configuration options.
490
+ * @param {boolean} [options.uppercase=false] - Whether to use uppercase.
491
+ * @param {boolean} [options.letters=true] - Include letter characters.
492
+ * @param {boolean} [options.digits=true] - Include digit characters.
493
+ * @returns {string} An alphanumeric string.
494
+ */
495
+ static alphanumeric(length = 10, options = {}) {
496
+ let pool = '';
497
+ if (options.letters !== false) {
498
+ pool += options.uppercase ? CHARSET_UPPERCASE : CHARSET_LOWERCASE;
499
+ }
500
+ if (options.digits !== false) pool += CHARSET_DIGITS;
501
+ if (!pool) pool = CHARSET_LOWERCASE + CHARSET_DIGITS;
502
+ let str = '';
503
+ for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
504
+ return str;
505
+ }
506
+
507
+ /**
508
+ * Random alpha-only string of `length` characters.
509
+ *
510
+ * @param {number} [length=8] - String length.
511
+ * @param {object} [options] - Configuration options.
512
+ * @param {boolean} [options.uppercase=false] - Use uppercase letters.
513
+ * @param {boolean} [options.mixed=false] - Mix upper and lower.
514
+ * @returns {string} An alphabetic string.
515
+ */
516
+ static alpha(length = 8, options = {}) {
517
+ let pool;
518
+ if (options.mixed) pool = CHARSET_LOWERCASE + CHARSET_UPPERCASE;
519
+ else if (options.uppercase) pool = CHARSET_UPPERCASE;
520
+ else pool = CHARSET_LOWERCASE;
521
+ let str = '';
522
+ for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
523
+ return str;
524
+ }
525
+
526
+ /** Random boolean. */
527
+ static boolean() { return rand() > 0.5; }
528
+
529
+ // -- Dates --------------------------------------------------------------
530
+
531
+ /**
532
+ * Random Date between start and end.
533
+ *
534
+ * @param {Date} [start] - Start offset.
535
+ * @param {Date} [end] - End value.
536
+ * @returns {Date} A random Date object.
537
+ */
538
+ static date(start = new Date(2020, 0, 1), end = new Date()) {
539
+ const ms = start.getTime() + rand() * (end.getTime() - start.getTime());
540
+ return new Date(ms);
541
+ }
542
+
543
+ /**
544
+ * Random ISO date string.
545
+ *
546
+ * @param {Date} [start] - Start offset.
547
+ * @param {Date} [end] - End value.
548
+ * @returns {string} An ISO 8601 date string.
549
+ */
550
+ static dateString(start, end) {
551
+ return Fake.date(start, end).toISOString();
552
+ }
553
+
554
+ /**
555
+ * Random date in the past.
556
+ *
557
+ * @param {object} [options] - Configuration options.
558
+ * @param {number} [options.years=1] - How many years back the window spans.
559
+ * @returns {Date} A Date in the past.
560
+ */
561
+ static datePast(options = {}) {
562
+ const years = options.years || 1;
563
+ const end = new Date();
564
+ const start = new Date(end);
565
+ start.setFullYear(start.getFullYear() - years);
566
+ return Fake.date(start, end);
567
+ }
568
+
569
+ /**
570
+ * Random date in the future.
571
+ *
572
+ * @param {object} [options] - Configuration options.
573
+ * @param {number} [options.years=1] - Year range offset.
574
+ * @returns {Date} A Date in the future.
575
+ */
576
+ static dateFuture(options = {}) {
577
+ const years = options.years || 1;
578
+ const start = new Date();
579
+ const end = new Date(start);
580
+ end.setFullYear(end.getFullYear() + years);
581
+ return Fake.date(start, end);
582
+ }
583
+
584
+ // -- Text ---------------------------------------------------------------
585
+
586
+ /**
587
+ * Random word from the lorem ipsum vocabulary.
588
+ *
589
+ * @param {object} [options] - Configuration options.
590
+ * @param {'lorem'|'hacker'|'adjective'|'noun'} [options.type='lorem'] - Content type variant.
591
+ * @returns {string} A single word.
592
+ */
593
+ static word(options = {}) {
594
+ const type = options.type || 'lorem';
595
+ if (type === 'hacker') return _pick(HACKER_NOUNS);
596
+ if (type === 'adjective') return _pick(ADJECTIVES);
597
+ if (type === 'noun') return _pick(NOUNS);
598
+ return _pick(LOREM_WORDS);
599
+ }
600
+
601
+ /**
602
+ * Array of n random words.
603
+ *
604
+ * @param {number} [n=3] - Count.
605
+ * @returns {string[]} Array of random words.
606
+ */
607
+ static words(n = 3) {
608
+ return Array.from({ length: n }, () => Fake.word());
609
+ }
610
+
611
+ /**
612
+ * Random sentence of `wordCount` words.
613
+ *
614
+ * @param {number|object} [wordCount] - Number of words.
615
+ * @returns {string} A sentence ending with a period.
616
+ */
617
+ static sentence(wordCount) {
618
+ const count = wordCount || _int(5, 15);
619
+ const words = Array.from({ length: count }, () => _pick(LOREM_WORDS));
620
+ words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
621
+ return words.join(' ') + '.';
622
+ }
623
+
624
+ /**
625
+ * Random paragraph of `sentences` sentences.
626
+ *
627
+ * @param {number} [sentences=3] - Number of sentences.
628
+ * @returns {string} A multi-sentence paragraph.
629
+ */
630
+ static paragraph(sentences = 3) {
631
+ return Array.from({ length: sentences }, () => Fake.sentence()).join(' ');
632
+ }
633
+
634
+ /**
635
+ * Hacker-speak phrase ("synthesize redundant protocols").
636
+ *
637
+ * @returns {string} A hacker-speak phrase.
638
+ */
639
+ static hackerPhrase() {
640
+ return `${_pick(HACKER_VERBS)} ${_pick(HACKER_ADJECTIVES)} ${_pick(HACKER_NOUNS)}`;
641
+ }
642
+
643
+ /**
644
+ * URL-friendly slug from n random words.
645
+ *
646
+ * @param {number} [words=3] - Number of words.
647
+ * @returns {string} e.g. 'forward-online-portal'
648
+ */
649
+ static slug(words = 3) {
650
+ return Array.from({ length: words }, () => _pick(ADJECTIVES.concat(NOUNS)))
651
+ .map(w => w.toLowerCase().replace(/[^a-z0-9]/g, '-'))
652
+ .join('-');
653
+ }
654
+
655
+ /**
656
+ * Random hashtag (no spaces, prefixed with #).
657
+ *
658
+ * @returns {string} e.g. '#resilientfuture'
659
+ */
660
+ static hashtag() {
661
+ return '#' + _pick(ADJECTIVES) + _pick(NOUNS);
662
+ }
663
+
664
+ // -- Person -------------------------------------------------------------
665
+
666
+ /**
667
+ * Random job title.
668
+ *
669
+ * @param {object} [options] - Configuration options.
670
+ * @param {boolean} [options.full=false] - Use a pre-built full title instead of generated.
671
+ * @returns {string} A job title string.
672
+ */
673
+ static jobTitle(options = {}) {
674
+ if (options.full) return _pick(JOB_TITLES);
675
+ return `${_pick(JOB_DESCRIPTORS)} ${_pick(JOB_AREAS)} ${_pick(JOB_TYPES)}`;
676
+ }
677
+
678
+ /** Random job area / department string (e.g. 'Engineering'). */
679
+ static jobArea() { return _pick(JOB_AREAS); }
680
+
681
+ /** Random job type noun (e.g. 'Manager'). */
682
+ static jobType() { return _pick(JOB_TYPES); }
683
+
684
+ /** Random job level descriptor (e.g. 'Senior'). */
685
+ static jobDescriptor() { return _pick(JOB_DESCRIPTORS); }
686
+
687
+ /**
688
+ * Random short biography string.
689
+ *
690
+ * @param {object} [options] - Configuration options.
691
+ * @param {'phrase'|'full'} [options.style='phrase'] - Style variant.
692
+ * @returns {string} A short biography phrase.
693
+ *
694
+ * @example
695
+ * Fake.bio(); // 'avid maker | living one commit at a time'
696
+ */
697
+ static bio(options = {}) {
698
+ if (options.style === 'full') {
699
+ return `${_pick(BIO_ADJECTIVES)} ${_pick(BIO_NOUNS)} | ${_pick(BIO_PHRASES)}`;
700
+ }
701
+ return _pick(BIO_PHRASES);
702
+ }
703
+
704
+ /** Random zodiac sign. */
705
+ static zodiacSign() { return _pick(ZODIAC_SIGNS); }
706
+
707
+ /**
708
+ * Random gender label.
709
+ *
710
+ * @param {object} [options] - Configuration options.
711
+ * @param {boolean} [options.binary=false] - Return only 'Male' or 'Female'.
712
+ * @returns {string} A gender label.
713
+ */
714
+ static gender(options = {}) {
715
+ if (options.binary) return rand() > 0.5 ? 'Male' : 'Female';
716
+ return _pick(GENDERS);
717
+ }
718
+
719
+ /** Random blood type (A+, B-, O+, AB+, …). */
720
+ static bloodType() { return _pick(BLOOD_TYPES); }
721
+
722
+ // -- Location ----------------------------------------------------------
723
+
724
+ /**
725
+ * Random city.
726
+ *
727
+ * @param {object} [options] - Configuration options.
728
+ * @param {string} [options.country] - Filter by ISO 3166-1 alpha-2 country code.
729
+ * @returns {string} A city name.
730
+ */
731
+ static city(options = {}) {
732
+ const pool = options.country
733
+ ? CITIES.filter(c => c.country === options.country.toUpperCase())
734
+ : CITIES;
735
+ return _pick(pool.length ? pool : CITIES).name;
736
+ }
737
+
738
+ /**
739
+ * Random country.
740
+ *
741
+ * @param {object} [options] - Configuration options.
742
+ * @param {boolean} [options.codeOnly=false] - Return ISO code only.
743
+ * @param {boolean} [options.full=false] - Return { name, code } object.
744
+ * @returns {string|{name:string,code:string}}
745
+ */
746
+ static country(options = {}) {
747
+ const entry = _pick(COUNTRIES);
748
+ if (options.codeOnly) return entry.code;
749
+ if (options.full) return { ...entry };
750
+ return entry.name;
751
+ }
752
+
753
+ /**
754
+ * Random US state.
755
+ *
756
+ * @param {object} [options] - Configuration options.
757
+ * @param {boolean} [options.abbr=false] - Return 2-letter abbreviation.
758
+ * @param {boolean} [options.full=false] - Return { name, abbr } object.
759
+ * @returns {string|{name:string,abbr:string}}
760
+ */
761
+ static state(options = {}) {
762
+ const entry = _pick(US_STATES);
763
+ if (options.abbr) return entry.abbr;
764
+ if (options.full) return { ...entry };
765
+ return entry.name;
766
+ }
767
+
768
+ /**
769
+ * Random postal / ZIP code.
770
+ *
771
+ * @param {object} [options] - Configuration options.
772
+ * @param {string} [options.countryCode='US'] - ISO country code.
773
+ * @returns {string} A postal code string.
774
+ */
775
+ static zipCode(options = {}) {
776
+ const code = (options.countryCode || 'US').toUpperCase();
777
+ const pattern = ZIP_PATTERNS[code] || ZIP_PATTERNS['US'];
778
+ return _expandTemplate(pattern);
779
+ }
780
+
781
+ /**
782
+ * Random latitude as a float in [-90, 90].
783
+ *
784
+ * @param {object} [options] - Configuration options.
785
+ * @param {number} [options.min=-90] - Minimum value.
786
+ * @param {number} [options.max=90] - Maximum value.
787
+ * @param {number} [options.decimals=6] - Decimal places.
788
+ * @returns {number} A latitude value.
789
+ */
790
+ static latitude(options = {}) {
791
+ return Fake.float(options.min ?? -90, options.max ?? 90, options.decimals ?? 6);
792
+ }
793
+
794
+ /**
795
+ * Random longitude as a float in [-180, 180].
796
+ *
797
+ * @param {object} [options] - Configuration options.
798
+ * @param {number} [options.min=-180] - Minimum value.
799
+ * @param {number} [options.max=180] - Maximum value.
800
+ * @param {number} [options.decimals=6] - Decimal places.
801
+ * @returns {number} A longitude value.
802
+ */
803
+ static longitude(options = {}) {
804
+ return Fake.float(options.min ?? -180, options.max ?? 180, options.decimals ?? 6);
805
+ }
806
+
807
+ /**
808
+ * Random { latitude, longitude } coordinate object.
809
+ *
810
+ * @param {object} [options] - Passed to latitude() and longitude().
811
+ * @returns {{ latitude: number, longitude: number }}
812
+ */
813
+ static coordinates(options = {}) {
814
+ return { latitude: Fake.latitude(options), longitude: Fake.longitude(options) };
815
+ }
816
+
817
+ /** Random IANA timezone identifier (e.g. 'America/New_York'). */
818
+ static timezone() { return _pick(TIMEZONES); }
819
+
820
+ /**
821
+ * Random street name (e.g. 'Oak Avenue').
822
+ *
823
+ * @returns {string} A street name.
824
+ */
825
+ static streetName() {
826
+ return `${_pick(STREET_NAMES)} ${_pick(STREET_TYPES)}`;
827
+ }
828
+
829
+ /**
830
+ * Random full street address.
831
+ *
832
+ * @param {object} [options] - Configuration options.
833
+ * @param {string} [options.countryCode='US'] - ISO country code.
834
+ * @param {'string'|'object'} [options.format='string'] - Output format.
835
+ * @returns {string|object} A formatted address string, or an address object when format is 'object'.
836
+ *
837
+ * @example
838
+ * Fake.address();
839
+ * // '742 Evergreen Terrace, Springfield, IL 62701'
840
+ *
841
+ * Fake.address({ format: 'object' });
842
+ * // { streetNumber, streetName, city, state, zipCode, country }
843
+ */
844
+ static address(options = {}) {
845
+ const code = (options.countryCode || 'US').toUpperCase();
846
+ const streetNum = _int(1, 9999);
847
+ const street = Fake.streetName();
848
+ const city = Fake.city({ country: code });
849
+ const zipCode = Fake.zipCode({ countryCode: code });
850
+ const stateEntry = code === 'US' ? _pick(US_STATES) : null;
851
+ const countryName = (COUNTRIES.find(c => c.code === code) || { name: code }).name;
852
+
853
+ if (options.format === 'object') {
854
+ return {
855
+ streetNumber: String(streetNum),
856
+ streetName: street,
857
+ city,
858
+ state: stateEntry ? stateEntry.abbr : null,
859
+ zipCode,
860
+ country: countryName,
861
+ };
862
+ }
863
+
864
+ if (stateEntry) {
865
+ return `${streetNum} ${street}, ${city}, ${stateEntry.abbr} ${zipCode}`;
866
+ }
867
+ return `${streetNum} ${street}, ${city} ${zipCode}, ${countryName}`;
868
+ }
869
+
870
+ // -- Commerce -----------------------------------------------------------
871
+
872
+ /**
873
+ * Random product name (adjective + material + noun).
874
+ *
875
+ * @param {object} [options] - Configuration options.
876
+ * @param {boolean} [options.withMaterial=true] - Include a material word.
877
+ * @returns {string} A product name.
878
+ */
879
+ static productName(options = {}) {
880
+ const withMaterial = options.withMaterial !== false;
881
+ if (withMaterial) {
882
+ return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_MATERIALS)} ${_pick(PRODUCT_NOUNS)}`;
883
+ }
884
+ return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_NOUNS)}`;
885
+ }
886
+
887
+ /** Random product category (e.g. 'Electronics'). */
888
+ static category() { return _pick(CATEGORIES); }
889
+
890
+ /** Random business department name. */
891
+ static department() { return _pick(DEPARTMENTS); }
892
+
893
+ /**
894
+ * Random company name.
895
+ *
896
+ * @param {object} [options] - Configuration options.
897
+ * @param {boolean} [options.suffix=true] - Append LLC / Inc. / etc.
898
+ * @returns {string} A company name.
899
+ */
900
+ static company(options = {}) {
901
+ const withSuffix = options.suffix !== false;
902
+ const name = `${_pick(COMPANY_ADJECTIVES)} ${_pick(COMPANY_NOUNS)}`;
903
+ return withSuffix ? `${name} ${_pick(COMPANY_SUFFIXES)}` : name;
904
+ }
905
+
906
+ /**
907
+ * Random price as a float with 2 decimal places.
908
+ *
909
+ * @param {object} [options] - Configuration options.
910
+ * @param {number} [options.min=0.99] - Minimum value.
911
+ * @param {number} [options.max=999.99] - Maximum value.
912
+ * @param {string} [options.symbol=''] - Prepend a currency symbol.
913
+ * @returns {string|number} - String when symbol is provided, number otherwise.
914
+ */
915
+ static price(options = {}) {
916
+ const val = Fake.float(options.min ?? 0.99, options.max ?? 999.99, 2);
917
+ return options.symbol ? `${options.symbol}${val.toFixed(2)}` : val;
918
+ }
919
+
920
+ /** Random industry sector name. */
921
+ static industry() { return _pick(INDUSTRIES); }
922
+
923
+ /** Random catch phrase buzzword phrase. */
924
+ static catchPhrase() {
925
+ return `${_pick(CATCH_PHRASE_ADJECTIVES)} ${_pick(CATCH_PHRASE_NOUNS)}`;
926
+ }
927
+
928
+ // -- Internet & Network -------------------------------------------------
929
+
930
+ /**
931
+ * Random email address (backward-compatible shorthand — same as email()).
932
+ * Accepts no arguments for historical use.
933
+ */
934
+
935
+ /** Random UUID v4. */
936
+ static uuid() { return _uuid(); }
937
+
938
+ /**
939
+ * Random domain name (adjective-noun.tld).
940
+ *
941
+ * @param {object} [options] - Configuration options.
942
+ * @param {string} [options.tld] - Force a specific TLD.
943
+ * @returns {string} A domain name.
944
+ */
945
+ static domainName(options = {}) {
946
+ const tld = options.tld || _pick(TLDS);
947
+ return `${_pick(DOMAIN_ADJECTIVES)}-${_pick(DOMAIN_NOUNS)}.${tld}`;
948
+ }
949
+
950
+ /**
951
+ * Random URL.
952
+ *
953
+ * @param {object} [options] - Configuration options.
954
+ * @param {'http'|'https'} [options.protocol='https'] - URL protocol.
955
+ * @param {boolean} [options.appendSlash=false] - Append trailing slash.
956
+ * @returns {string} A URL string.
957
+ */
958
+ static url(options = {}) {
959
+ const proto = options.protocol || 'https';
960
+ const path = options.noPath ? '' : `/${Fake.word()}`;
961
+ const slash = options.appendSlash ? '/' : '';
962
+ return `${proto}://${Fake.domainName()}${path}${slash}`;
963
+ }
964
+
965
+ /**
966
+ * Random IPv4 address.
967
+ *
968
+ * @param {object} [options] - Configuration options.
969
+ * @param {'any'|'private-a'|'private-b'|'private-c'|'loopback'} [options.network='any'] - Network type.
970
+ * @returns {string} An IPv4 address.
971
+ */
972
+ static ip(options = {}) {
973
+ const net = options.network || 'any';
974
+ if (net === 'loopback') return `127.0.0.${_int(1, 254)}`;
975
+ if (net === 'private-a') return `10.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
976
+ if (net === 'private-b') return `172.${_int(16,31)}.${_int(0,255)}.${_int(1,254)}`;
977
+ if (net === 'private-c') return `192.168.${_int(0,255)}.${_int(1,254)}`;
978
+ return `${_int(1,254)}.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
979
+ }
980
+
981
+ /**
982
+ * Random IPv6 address.
983
+ *
984
+ * @returns {string} An IPv6 address.
985
+ */
986
+ static ipv6() {
987
+ const groups = Array.from({ length: 8 }, () =>
988
+ _int(0, 65535).toString(16).padStart(4, '0')
989
+ );
990
+ return groups.join(':');
991
+ }
992
+
993
+ /**
994
+ * Random MAC address.
995
+ *
996
+ * @param {object} [options] - Configuration options.
997
+ * @param {':'|'-'|''} [options.separator=':'] - Separator character.
998
+ * @param {boolean} [options.realisticOUI=false] - Use a real vendor OUI prefix.
999
+ * @returns {string} A MAC address string.
1000
+ */
1001
+ static mac(options = {}) {
1002
+ const sep = options.separator ?? ':';
1003
+ if (options.realisticOUI) {
1004
+ const oui = _pick(MAC_OUIS).replace(/:/g, sep);
1005
+ const rest = Array.from({ length: 3 }, () =>
1006
+ _int(0, 255).toString(16).padStart(2, '0')
1007
+ ).join(sep);
1008
+ return `${oui}${sep}${rest}`;
1009
+ }
1010
+ return Array.from({ length: 6 }, () =>
1011
+ _int(0, 255).toString(16).padStart(2, '0')
1012
+ ).join(sep);
1013
+ }
1014
+
1015
+ /**
1016
+ * Random network port number.
1017
+ *
1018
+ * @param {object} [options] - Configuration options.
1019
+ * @param {'well-known'|'registered'|'dynamic'|'any'} [options.range='any'] - Address range.
1020
+ * well-known → 1–1023
1021
+ * registered → 1024–49151
1022
+ * dynamic → 49152–65535
1023
+ * any → 1–65535
1024
+ * @returns {number} A port number.
1025
+ */
1026
+ static port(options = {}) {
1027
+ const range = options.range || 'any';
1028
+ if (range === 'well-known') return _int(1, 1023);
1029
+ if (range === 'registered') return _int(1024, 49151);
1030
+ if (range === 'dynamic') return _int(49152, 65535);
1031
+ return _int(1, 65535);
1032
+ }
1033
+
1034
+ /**
1035
+ * Random HTTP method.
1036
+ *
1037
+ * @param {object} [options] - Configuration options.
1038
+ * @param {string[]} [options.methods] - Restrict to specific methods.
1039
+ * @returns {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'} An HTTP method string.
1040
+ */
1041
+ static httpMethod(options = {}) {
1042
+ const pool = options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
1043
+ return _pick(pool);
1044
+ }
1045
+
1046
+ /**
1047
+ * Random user agent string (realistic browser/client).
1048
+ *
1049
+ * @returns {string} A user agent string.
1050
+ */
1051
+ static userAgent() { return _pick(USER_AGENTS); }
1052
+
1053
+ /**
1054
+ * Random password-like string. NOT suitable for real passwords — uses a
1055
+ * PRNG seeded from Math.random, not a CSPRNG.
1056
+ *
1057
+ * @param {object} [options] - Configuration options.
1058
+ * @param {number} [options.length=16] - Character count.
1059
+ * @param {boolean} [options.uppercase=true] - Include uppercase letters.
1060
+ * @param {boolean} [options.lowercase=true] - Include lowercase letters.
1061
+ * @param {boolean} [options.digits=true] - Include digits.
1062
+ * @param {boolean} [options.special=false] - Include special characters.
1063
+ * @param {string} [options.prefix=''] - Prepend a prefix.
1064
+ * @returns {string} A password string.
1065
+ */
1066
+ static password(options = {}) {
1067
+ const len = options.length ?? 16;
1068
+ const prefix = options.prefix ?? '';
1069
+ let pool = '';
1070
+ if (options.lowercase !== false) pool += CHARSET_LOWERCASE;
1071
+ if (options.uppercase !== false) pool += CHARSET_UPPERCASE;
1072
+ if (options.digits !== false) pool += CHARSET_DIGITS;
1073
+ if (options.special === true) pool += CHARSET_SPECIAL;
1074
+ pool = pool || (CHARSET_LOWERCASE + CHARSET_UPPERCASE + CHARSET_DIGITS);
1075
+ const body = Array.from({ length: Math.max(0, len - prefix.length) }, () =>
1076
+ pool[Math.floor(rand() * pool.length)]
1077
+ ).join('');
1078
+ return `${prefix}${body}`;
1079
+ }
1080
+
1081
+ // -- Colors -------------------------------------------------------------
1082
+
1083
+ /**
1084
+ * Random hex color code.
1085
+ *
1086
+ * @returns {string} e.g. '#3a7f2b'
1087
+ */
1088
+ static color() {
1089
+ return '#' + Math.floor(rand() * 16777215).toString(16).padStart(6, '0');
1090
+ }
1091
+
1092
+ /**
1093
+ * Random RGB color object or string.
1094
+ *
1095
+ * @param {object} [options] - Configuration options.
1096
+ * @param {'object'|'css'} [options.format='css'] - Output format.
1097
+ * @returns {{ r:number, g:number, b:number }|string}
1098
+ */
1099
+ static rgb(options = {}) {
1100
+ const r = _int(0, 255), g = _int(0, 255), b = _int(0, 255);
1101
+ if (options.format === 'object') return { r, g, b };
1102
+ return `rgb(${r}, ${g}, ${b})`;
1103
+ }
1104
+
1105
+ /**
1106
+ * Random HSL color string or object.
1107
+ *
1108
+ * @param {object} [options] - Configuration options.
1109
+ * @param {'object'|'css'} [options.format='css'] - Output format.
1110
+ * @returns {{ h:number, s:number, l:number }|string}
1111
+ */
1112
+ static hsl(options = {}) {
1113
+ const h = _int(0, 360), s = _int(20, 100), l = _int(20, 80);
1114
+ if (options.format === 'object') return { h, s, l };
1115
+ return `hsl(${h}, ${s}%, ${l}%)`;
1116
+ }
1117
+
1118
+ // -- Helpers ------------------------------------------------------------
1119
+
1120
+ /**
1121
+ * Pick a random element from an array.
1122
+ *
1123
+ * @template T
1124
+ * @param {T[]} arr - Input array.
1125
+ * @returns {T} A random element from the array.
1126
+ */
1127
+ static pick(arr) { return _pick(arr); }
1128
+
1129
+ /**
1130
+ * Pick n random elements from an array (no duplicates).
1131
+ *
1132
+ * @template T
1133
+ * @param {T[]} arr - Input array.
1134
+ * @param {number} n - Count.
1135
+ * @returns {T[]} An array of n distinct random elements.
1136
+ */
1137
+ static pickMany(arr, n) { return _pickMany(arr, n); }
1138
+
1139
+ /**
1140
+ * Shuffle an array in-place using Fisher-Yates and return it.
1141
+ *
1142
+ * @template T
1143
+ * @param {T[]} arr - Input array.
1144
+ * @returns {T[]} The shuffled array (mutated in-place).
1145
+ */
1146
+ static shuffle(arr) {
1147
+ for (let i = arr.length - 1; i > 0; i--) {
1148
+ const j = Math.floor(rand() * (i + 1));
1149
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1150
+ }
1151
+ return arr;
1152
+ }
1153
+
1154
+ /**
1155
+ * Random JSON-safe object (useful as a quick fixture value).
1156
+ *
1157
+ * @returns {{ key: string, value: string, count: number, active: boolean }}
1158
+ */
1159
+ static json() {
1160
+ return {
1161
+ key: Fake.word(),
1162
+ value: Fake.sentence(3),
1163
+ count: _int(1, 100),
1164
+ active: Fake.boolean(),
1165
+ };
1166
+ }
1167
+
1168
+ /**
1169
+ * Random element from an enum-like array, validated at call time.
1170
+ *
1171
+ * @template T
1172
+ * @param {readonly T[]} values - Array of values.
1173
+ * @returns {T} A random element from the values array.
1174
+ */
1175
+ static enumValue(values) {
1176
+ if (!Array.isArray(values) || values.length === 0) {
1177
+ throw new Error('Fake.enumValue: requires a non-empty array');
1178
+ }
1179
+ return _pick(values);
1180
+ }
1181
+ }
1182
+
1183
+ // Static uniqueness tracker — shared across all calls in the process lifetime
1184
+ Fake._tracker = new UniqueTracker();
1185
+
1186
+ module.exports = { Fake };