@zahid15/mockline 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3628 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'fs';
3
+ import { createRequire } from 'module';
4
+ import { writeFile, readFile, mkdir, appendFile, rename } from 'fs/promises';
5
+ import path, { resolve, extname, dirname } from 'path';
6
+ import cac from 'cac';
7
+ import pc from 'picocolors';
8
+ import { z, ZodError } from 'zod';
9
+ import { parse } from 'yaml';
10
+ import SwaggerParser from '@apidevtools/swagger-parser';
11
+ import { createServer } from 'http';
12
+ import { watch } from 'chokidar';
13
+ import { isIP } from 'net';
14
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
15
+ import Ajv from 'ajv';
16
+ import addFormats from 'ajv-formats';
17
+ import Ajv2020 from 'ajv/dist/2020.js';
18
+ import { Faker, en } from '@faker-js/faker';
19
+ import { lookup } from 'dns/promises';
20
+
21
+ var nonNegative = z.number().finite().min(0);
22
+ var responseShape = z.object({
23
+ status: z.number().int().min(100).max(599).optional(),
24
+ body: z.unknown().optional(),
25
+ headers: z.record(z.string()).optional(),
26
+ delay: nonNegative.optional()
27
+ });
28
+ var conditionSchema = z.object({
29
+ location: z.enum(["query", "header", "body"]),
30
+ name: z.string().min(1),
31
+ op: z.enum(["eq", "contains", "regex"]).default("eq"),
32
+ value: z.unknown()
33
+ });
34
+ var overrideSchema = z.object({
35
+ status: z.number().int().min(100).max(599).optional(),
36
+ body: z.unknown().optional(),
37
+ headers: z.record(z.string()).optional(),
38
+ delay: nonNegative.optional(),
39
+ sequence: z.array(responseShape).min(1).optional(),
40
+ loop: z.boolean().default(false),
41
+ when: conditionSchema.optional(),
42
+ response: responseShape.optional(),
43
+ rules: z.array(z.object({ when: conditionSchema, response: responseShape })).optional()
44
+ });
45
+ var latencySchema = z.union([
46
+ nonNegative,
47
+ z.tuple([nonNegative, nonNegative]).refine(([min, max]) => min <= max, {
48
+ message: "range minimum must not exceed maximum"
49
+ }),
50
+ z.object({
51
+ fixed: nonNegative.optional(),
52
+ min: nonNegative.optional(),
53
+ max: nonNegative.optional(),
54
+ distribution: z.enum(["uniform", "normal"]).default("uniform")
55
+ }).refine(
56
+ (value) => value.fixed !== void 0 || value.min !== void 0 && value.max !== void 0,
57
+ {
58
+ message: "provide fixed or both min and max"
59
+ }
60
+ ).refine(
61
+ (value) => value.min === void 0 || value.max === void 0 || value.min <= value.max,
62
+ {
63
+ message: "range minimum must not exceed maximum",
64
+ path: ["min"]
65
+ }
66
+ )
67
+ ]);
68
+ var configSchema = z.object({
69
+ port: z.number().int().min(0).max(65535).default(4010),
70
+ host: z.string().min(1).default("127.0.0.1"),
71
+ seed: z.number().int().optional(),
72
+ prefer: z.enum(["example", "generate"]).default("example"),
73
+ latency: latencySchema.default(0),
74
+ errors: z.object({
75
+ rate: z.number().min(0).max(1).default(0),
76
+ statuses: z.array(z.number().int().min(400).max(599)).min(1).default([500, 503]),
77
+ perOperationRates: z.record(z.number().min(0).max(1)).default({})
78
+ }).default({}),
79
+ validate: z.enum(["off", "warn", "strict"]).default("warn"),
80
+ stateful: z.object({
81
+ enabled: z.boolean().default(false),
82
+ seedItems: z.number().int().min(0).default(3),
83
+ idProperty: z.string().min(1).default("id")
84
+ }).default({}),
85
+ overrides: z.record(overrideSchema).default({}),
86
+ proxy: z.object({
87
+ url: z.string().url().refine((value) => /^https?:\/\//i.test(value), {
88
+ message: "must use http or https"
89
+ }).optional(),
90
+ all: z.boolean().default(false),
91
+ allowPrivate: z.boolean().default(false),
92
+ timeoutMs: z.number().int().positive().default(5e3),
93
+ recordFile: z.string().optional()
94
+ }).refine((value) => !value.all || Boolean(value.url), {
95
+ message: "proxy.url is required when proxy.all is true",
96
+ path: ["url"]
97
+ }).default({}),
98
+ cors: z.object({
99
+ enabled: z.boolean().default(true),
100
+ origins: z.array(z.string()).default(["*"])
101
+ }).default({}),
102
+ recording: z.object({
103
+ enabled: z.boolean().default(true),
104
+ file: z.string().optional(),
105
+ maxRequests: z.number().int().positive().default(500),
106
+ redactHeaders: z.array(z.string()).default(["authorization", "cookie", "set-cookie", "x-api-key", "api-key", "x-auth-token"])
107
+ }).default({}),
108
+ generation: z.object({
109
+ hints: z.union([
110
+ z.boolean(),
111
+ z.record(
112
+ z.union([
113
+ z.string(),
114
+ z.number(),
115
+ z.boolean(),
116
+ z.null(),
117
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
118
+ ])
119
+ )
120
+ ]).default(true),
121
+ counter: z.boolean().default(false),
122
+ maxDepth: z.number().int().min(0).max(20).default(3),
123
+ minArrayItems: z.number().int().min(0).default(1),
124
+ maxArrayItems: z.number().int().min(0).default(3)
125
+ }).refine((value) => value.minArrayItems <= value.maxArrayItems, {
126
+ message: "minArrayItems must not exceed maxArrayItems",
127
+ path: ["minArrayItems"]
128
+ }).default({}),
129
+ controlHeaders: z.object({ allow: z.boolean().default(true) }).default({}),
130
+ inspector: z.object({ enabled: z.boolean().optional() }).default({}),
131
+ enforceAuth: z.boolean().default(false),
132
+ requestBodyLimit: z.number().int().positive().default(1048576),
133
+ requestTimeoutMs: z.number().int().positive().default(3e4)
134
+ });
135
+ configSchema.parse({});
136
+
137
+ // src/errors.ts
138
+ var MocklineError = class extends Error {
139
+ exitCode;
140
+ details;
141
+ constructor(message, exitCode = 3, details) {
142
+ super(message);
143
+ this.name = "MocklineError";
144
+ this.exitCode = exitCode;
145
+ this.details = details;
146
+ }
147
+ };
148
+ function problem(status, title, detail, extra = {}) {
149
+ return {
150
+ type: "about:blank",
151
+ title,
152
+ status,
153
+ ...detail ? { detail } : {},
154
+ ...extra
155
+ };
156
+ }
157
+
158
+ // src/config/load.ts
159
+ function formatConfigErrors(error) {
160
+ return error.issues.map((issue) => {
161
+ const path2 = issue.path.length > 0 ? issue.path.join(".") : "<root>";
162
+ return `${path2}: ${issue.message}`;
163
+ }).join("\n");
164
+ }
165
+ async function parseConfigText(text, fileName = "mockline.yaml") {
166
+ let raw;
167
+ try {
168
+ raw = extname(fileName).toLowerCase() === ".json" ? JSON.parse(text) : parse(text);
169
+ } catch (error) {
170
+ const reason = error instanceof Error ? error.message : String(error);
171
+ throw new MocklineError(`Invalid config ${fileName}: ${reason}`, 1);
172
+ }
173
+ const parsed = configSchema.safeParse(raw);
174
+ if (!parsed.success) {
175
+ throw new MocklineError(
176
+ `Invalid config ${fileName}:
177
+ ${formatConfigErrors(parsed.error)}`,
178
+ 1,
179
+ parsed.error
180
+ );
181
+ }
182
+ return parsed.data;
183
+ }
184
+ async function loadConfig(file) {
185
+ const candidates = file ? [resolve(file)] : [resolve("mockline.yaml"), resolve("mockline.yml"), resolve("mockline.json")];
186
+ const selected = candidates.find((candidate) => existsSync(candidate));
187
+ if (!selected) {
188
+ if (file) throw new MocklineError(`Config file not found: ${file}`, 1);
189
+ return { config: configSchema.parse({}) };
190
+ }
191
+ let text;
192
+ try {
193
+ text = await readFile(selected, "utf8");
194
+ } catch (error) {
195
+ const reason = error instanceof Error ? error.message : String(error);
196
+ throw new MocklineError(`Cannot read config ${selected}: ${reason}`, 1);
197
+ }
198
+ return { config: await parseConfigText(text, selected), path: selected };
199
+ }
200
+ function createLogger(options = {}) {
201
+ const quiet = options.quiet ?? false;
202
+ const verbose = options.verbose ?? false;
203
+ const color = options.color ?? process.stdout.isTTY;
204
+ const paint = (fn, value) => color ? fn(value) : value;
205
+ return {
206
+ info(message) {
207
+ if (!quiet) console.log(paint(pc.cyan, "mockline"), message);
208
+ },
209
+ warn(message) {
210
+ if (!quiet) console.warn(paint(pc.yellow, "warning"), message);
211
+ },
212
+ error(message) {
213
+ console.error(paint(pc.red, "error"), message);
214
+ },
215
+ debug(message) {
216
+ if (verbose && !quiet) console.log(paint(pc.dim, "debug"), message);
217
+ }
218
+ };
219
+ }
220
+ function isRecord(value) {
221
+ return typeof value === "object" && value !== null && !Array.isArray(value);
222
+ }
223
+ function operationEntries(document) {
224
+ const entries = [];
225
+ const paths = document.paths ?? {};
226
+ const methods = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
227
+ for (const [routePath, pathItemUnknown] of Object.entries(paths)) {
228
+ if (!isRecord(pathItemUnknown)) continue;
229
+ for (const [method, operation] of Object.entries(pathItemUnknown)) {
230
+ if (methods.has(method.toLowerCase()) && isRecord(operation)) {
231
+ entries.push({ path: routePath, method: method.toUpperCase(), operation });
232
+ }
233
+ }
234
+ }
235
+ return entries;
236
+ }
237
+ function getResponseDetails(operation) {
238
+ const responses = isRecord(operation.responses) ? operation.responses : {};
239
+ let hasExample2 = false;
240
+ let hasSchema = false;
241
+ for (const responseUnknown of Object.values(responses)) {
242
+ if (!isRecord(responseUnknown) || !isRecord(responseUnknown.content)) continue;
243
+ for (const mediaUnknown of Object.values(responseUnknown.content)) {
244
+ if (!isRecord(mediaUnknown)) continue;
245
+ if ("example" in mediaUnknown || isRecord(mediaUnknown.examples) && Object.keys(mediaUnknown.examples).length > 0) {
246
+ hasExample2 = true;
247
+ }
248
+ if (isRecord(mediaUnknown.schema)) hasSchema = true;
249
+ }
250
+ }
251
+ return { hasExample: hasExample2, hasSchema };
252
+ }
253
+ function inspectSpec(document) {
254
+ const warnings = [];
255
+ for (const entry of operationEntries(document)) {
256
+ const { hasExample: hasExample2, hasSchema } = getResponseDetails(entry.operation);
257
+ const responses = isRecord(entry.operation.responses) ? entry.operation.responses : {};
258
+ for (const response of Object.values(responses)) {
259
+ if (!isRecord(response) || !isRecord(response.content)) continue;
260
+ for (const [mediaType, media] of Object.entries(response.content)) {
261
+ if (!isRecord(media)) continue;
262
+ const json2 = mediaType.toLowerCase().includes("json");
263
+ const schema = isRecord(media.schema) ? media.schema : void 0;
264
+ const hasMediaExample = "example" in media || isRecord(media.examples) && Object.keys(media.examples).length > 0 || Boolean(
265
+ schema && ("example" in schema || Array.isArray(schema.examples) && schema.examples.length > 0)
266
+ );
267
+ if (!json2 && !hasMediaExample)
268
+ warnings.push({
269
+ message: `Mockline cannot generate ${mediaType} responses; add an example.`,
270
+ path: entry.path
271
+ });
272
+ }
273
+ }
274
+ if (!hasExample2 && !hasSchema) {
275
+ warnings.push({
276
+ message: "No response schema or example; Mockline can only return an empty/default body.",
277
+ operationId: typeof entry.operation.operationId === "string" ? entry.operation.operationId : void 0,
278
+ path: entry.path
279
+ });
280
+ }
281
+ if (entry.operation.callbacks)
282
+ warnings.push({ message: "Callbacks are not supported.", path: entry.path });
283
+ if (entry.operation.links)
284
+ warnings.push({
285
+ message: "Response links are ignored by the mock server.",
286
+ path: entry.path
287
+ });
288
+ }
289
+ if (document.webhooks && isRecord(document.webhooks) && Object.keys(document.webhooks).length > 0) {
290
+ warnings.push({ message: "Webhooks are not served by Mockline." });
291
+ }
292
+ return warnings;
293
+ }
294
+ async function loadSpec(file) {
295
+ const absoluteFile = path.resolve(file);
296
+ let text;
297
+ try {
298
+ text = await readFile(absoluteFile, "utf8");
299
+ } catch (error) {
300
+ const reason = error instanceof Error ? error.message : String(error);
301
+ throw new MocklineError(`Cannot read OpenAPI spec ${file}: ${reason}`, 1);
302
+ }
303
+ let parsed;
304
+ try {
305
+ parsed = /\.json$/i.test(absoluteFile) ? JSON.parse(text) : parse(text);
306
+ } catch (error) {
307
+ const reason = error instanceof Error ? error.message : String(error);
308
+ throw new MocklineError(`Cannot parse OpenAPI spec ${file}: ${reason}`, 1);
309
+ }
310
+ if (!isRecord(parsed))
311
+ throw new MocklineError("OpenAPI document must be a YAML or JSON object.", 1);
312
+ if (parsed.swagger === "2.0" || "swagger" in parsed) {
313
+ throw new MocklineError(
314
+ "Swagger 2.0 is not supported. Convert the document to OpenAPI 3.x (for example, with swagger2openapi) and try again.",
315
+ 1
316
+ );
317
+ }
318
+ if (typeof parsed.openapi !== "string" || !parsed.openapi.startsWith("3.")) {
319
+ throw new MocklineError("Only OpenAPI 3.0 and 3.1 documents are supported.", 1);
320
+ }
321
+ let dereferenced;
322
+ try {
323
+ const parser = new SwaggerParser();
324
+ dereferenced = await parser.validate(absoluteFile, {
325
+ dereference: { circular: "ignore" }
326
+ });
327
+ } catch (error) {
328
+ const reason = error instanceof Error ? error.message : String(error);
329
+ throw new MocklineError(`Invalid OpenAPI spec ${file}: ${reason}`, 1, error);
330
+ }
331
+ const document = dereferenced;
332
+ const warnings = inspectSpec(document);
333
+ return { file: absoluteFile, document, dereferenced, warnings };
334
+ }
335
+
336
+ // src/router.ts
337
+ var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
338
+ function isRecord2(value) {
339
+ return typeof value === "object" && value !== null && !Array.isArray(value);
340
+ }
341
+ function normalizePath(value) {
342
+ if (value === "" || value === "/") return "/";
343
+ const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
344
+ const trimmed = withLeadingSlash.replace(/\/+$/, "");
345
+ return trimmed || "/";
346
+ }
347
+ function replaceServerVariables(url, variables) {
348
+ return url.replace(
349
+ /\{([^}]+)\}/g,
350
+ (_match, name) => encodeURIComponent(variables?.[name]?.default ?? "")
351
+ );
352
+ }
353
+ function serverPath(url, variables) {
354
+ const replaced = replaceServerVariables(url, variables);
355
+ try {
356
+ return normalizePath(new URL(replaced, "http://mockline.local").pathname);
357
+ } catch {
358
+ return "/";
359
+ }
360
+ }
361
+ function routeForPath(base, pathTemplate) {
362
+ const cleanBase = base === "/" ? "" : normalizePath(base);
363
+ const cleanPath = normalizePath(pathTemplate);
364
+ return normalizePath(`${cleanBase}${cleanPath === "/" ? "" : cleanPath}`);
365
+ }
366
+ function compilePath(template) {
367
+ const normalized = normalizePath(template);
368
+ const parts = normalized.split("/").filter(Boolean);
369
+ const names = [];
370
+ let staticSegments = 0;
371
+ const pattern = parts.map((part) => {
372
+ const match = /^\{([^{}]+)\}$/.exec(part);
373
+ if (match) {
374
+ names.push(match[1] ?? "");
375
+ return "([^/]+)";
376
+ }
377
+ staticSegments += 1;
378
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
379
+ }).join("/");
380
+ const regex = new RegExp(`^/${pattern}${parts.length === 0 ? "" : ""}/?$`);
381
+ return {
382
+ regex,
383
+ parameterNames: names,
384
+ staticSegments,
385
+ score: staticSegments * 100 + parts.length
386
+ };
387
+ }
388
+ function toMatchedRoute(route) {
389
+ return {
390
+ method: route.method,
391
+ path: route.path,
392
+ routePath: route.routePath,
393
+ operationId: route.operationId,
394
+ operation: route.operation,
395
+ pathItem: route.pathItem,
396
+ parameters: route.parameters,
397
+ score: route.score
398
+ };
399
+ }
400
+ function operationId(operation, method, path2) {
401
+ return typeof operation.operationId === "string" && operation.operationId.length > 0 ? operation.operationId : `${method.toLowerCase()}_${path2.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "") || "root"}`;
402
+ }
403
+ function combineParameters(pathItem, operation) {
404
+ const merged = /* @__PURE__ */ new Map();
405
+ for (const parameter of [...pathItem.parameters ?? [], ...operation.parameters ?? []]) {
406
+ if (parameter && typeof parameter.name === "string" && typeof parameter.in === "string") {
407
+ merged.set(
408
+ `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`,
409
+ parameter
410
+ );
411
+ }
412
+ }
413
+ return [...merged.values()];
414
+ }
415
+ function createRouter(document) {
416
+ const routes = [];
417
+ const paths = document.paths ?? {};
418
+ for (const [pathTemplate, pathItemUnknown] of Object.entries(paths)) {
419
+ if (!isRecord2(pathItemUnknown)) continue;
420
+ const pathItem = pathItemUnknown;
421
+ for (const method of HTTP_METHODS) {
422
+ const operationUnknown = pathItemUnknown[method];
423
+ if (!isRecord2(operationUnknown)) continue;
424
+ const operation = operationUnknown;
425
+ const operationServers = Array.isArray(operation.servers) ? operation.servers : void 0;
426
+ const pathServers = Array.isArray(pathItem.servers) ? pathItem.servers : void 0;
427
+ const servers = operationServers ?? pathServers ?? document.servers ?? [];
428
+ const bases = servers.length > 0 ? servers.map((server) => serverPath(server.url, server.variables)) : ["/"];
429
+ for (const base of bases) {
430
+ const routePath = routeForPath(base, pathTemplate);
431
+ const compiled = compilePath(routePath);
432
+ routes.push({
433
+ method: method.toUpperCase(),
434
+ path: routePath,
435
+ routePath,
436
+ operationId: operationId(operation, method, routePath),
437
+ operation,
438
+ pathItem,
439
+ parameters: combineParameters(pathItem, operation),
440
+ score: compiled.score,
441
+ regex: compiled.regex,
442
+ parameterNames: compiled.parameterNames,
443
+ staticSegments: compiled.staticSegments
444
+ });
445
+ }
446
+ }
447
+ }
448
+ routes.sort(
449
+ (a, b) => b.staticSegments - a.staticSegments || b.score - a.score || a.path.localeCompare(b.path)
450
+ );
451
+ const publicRoutes = routes.map(toMatchedRoute);
452
+ const matchesPath = (route, pathname) => {
453
+ const normalized = normalizePath(pathname);
454
+ const match = route.regex.exec(normalized);
455
+ if (!match) return void 0;
456
+ const values = {};
457
+ for (const [index, name] of route.parameterNames.entries()) {
458
+ const encoded = match[index + 1];
459
+ if (encoded === void 0) continue;
460
+ try {
461
+ values[name] = decodeURIComponent(encoded);
462
+ } catch {
463
+ return void 0;
464
+ }
465
+ }
466
+ return values;
467
+ };
468
+ return {
469
+ routes: publicRoutes,
470
+ match(method, pathname) {
471
+ const upper = method.toUpperCase();
472
+ for (const route of routes) {
473
+ if (route.method !== upper) continue;
474
+ const params = matchesPath(route, pathname);
475
+ if (params) {
476
+ return { route: toMatchedRoute(route), params };
477
+ }
478
+ }
479
+ return void 0;
480
+ },
481
+ methodsForPath(pathname) {
482
+ const methods = /* @__PURE__ */ new Set();
483
+ for (const route of routes) if (matchesPath(route, pathname)) methods.add(route.method);
484
+ return [...methods].sort();
485
+ },
486
+ nearestPath(pathname) {
487
+ const normalized = normalizePath(pathname);
488
+ let best;
489
+ for (const route of routes) {
490
+ const routeParts = route.path.split("/").filter(Boolean);
491
+ const actualParts = normalized.split("/").filter(Boolean);
492
+ let common = 0;
493
+ for (let i = 0; i < Math.min(routeParts.length, actualParts.length); i += 1) {
494
+ if (routeParts[i] === actualParts[i] || /^\{[^{}]+\}$/.test(routeParts[i] ?? ""))
495
+ common += 1;
496
+ else break;
497
+ }
498
+ const distance = common * 10 - Math.abs(routeParts.length - actualParts.length);
499
+ if (!best || distance > best.distance) best = { route, distance };
500
+ }
501
+ return best?.route.path;
502
+ }
503
+ };
504
+ }
505
+
506
+ // src/spec/summary.ts
507
+ function defaultStatus(responses) {
508
+ if (!responses || Object.keys(responses).length === 0) return "200";
509
+ const success = Object.keys(responses).filter((key) => /^2\d\d$/.test(key)).sort((a, b) => Number(a) - Number(b));
510
+ if (success[0]) return success[0];
511
+ if (responses.default) return "default";
512
+ return "200";
513
+ }
514
+ function hasExample(response) {
515
+ if (!response?.content) return false;
516
+ return Object.values(response.content).some(
517
+ (media) => "example" in media || Boolean(media.examples && Object.keys(media.examples).length > 0) || Boolean(
518
+ media.schema && ("example" in media.schema || Array.isArray(media.schema.examples) && media.schema.examples.length > 0)
519
+ )
520
+ );
521
+ }
522
+ function summarizeSpec(spec, overrideKeys = []) {
523
+ const router = createRouter(spec.document);
524
+ return router.routes.map((route) => {
525
+ const responses = route.operation.responses;
526
+ const status = defaultStatus(responses);
527
+ const response = responses?.[status] ?? responses?.default;
528
+ const override = overrideKeys.some(
529
+ (key) => key === route.operationId || key.toUpperCase() === `${route.method} ${route.path}`.toUpperCase()
530
+ );
531
+ const source = override ? "override" : hasExample(response) ? "example" : "schema generation";
532
+ return {
533
+ method: route.method,
534
+ path: route.path,
535
+ operationId: route.operationId,
536
+ defaultStatus: status,
537
+ source
538
+ };
539
+ });
540
+ }
541
+
542
+ // src/generate/rng.ts
543
+ function hashSeed(input) {
544
+ const text = String(input);
545
+ let hash = 2166136261;
546
+ for (let index = 0; index < text.length; index += 1) {
547
+ hash ^= text.charCodeAt(index);
548
+ hash = Math.imul(hash, 16777619);
549
+ }
550
+ return hash >>> 0;
551
+ }
552
+ function deriveSeed(seed, ...parts) {
553
+ return hashSeed(`${seed ?? 0}|${parts.join("|")}`);
554
+ }
555
+ var SeededRandom = class {
556
+ state;
557
+ constructor(seed) {
558
+ this.state = seed >>> 0 || 1831565813;
559
+ }
560
+ next() {
561
+ this.state = this.state + 1831565813 | 0;
562
+ let value = this.state;
563
+ value = Math.imul(value ^ value >>> 15, value | 1);
564
+ value ^= value + Math.imul(value ^ value >>> 7, value | 61);
565
+ return ((value ^ value >>> 14) >>> 0) / 4294967296;
566
+ }
567
+ int(min, max) {
568
+ const low = Math.ceil(min);
569
+ const high = Math.floor(max);
570
+ if (high <= low) return low;
571
+ return low + Math.floor(this.next() * (high - low + 1));
572
+ }
573
+ float(min, max) {
574
+ if (max <= min) return min;
575
+ return min + this.next() * (max - min);
576
+ }
577
+ bool(probability = 0.5) {
578
+ return this.next() < probability;
579
+ }
580
+ pick(items) {
581
+ if (items.length === 0) return void 0;
582
+ return items[this.int(0, items.length - 1)];
583
+ }
584
+ };
585
+ var ROOT_ID = "https://mockline.local/openapi.json";
586
+ function cloneSchema(value, rootId, seen = /* @__PURE__ */ new WeakMap()) {
587
+ if (Array.isArray(value)) return value.map((item) => cloneSchema(item, rootId, seen));
588
+ if (typeof value !== "object" || value === null) return value;
589
+ const prior = seen.get(value);
590
+ if (prior) return prior;
591
+ const output = {};
592
+ seen.set(value, output);
593
+ for (const [key, child] of Object.entries(value)) {
594
+ if (key === "$ref" && typeof child === "string" && child.startsWith("#/")) {
595
+ output[key] = `${rootId}${child}`;
596
+ } else if ((key === "exclusiveMinimum" || key === "exclusiveMaximum") && child === false) {
597
+ continue;
598
+ } else if (key === "exclusiveMinimum" && child === true) {
599
+ const parent = value;
600
+ if (typeof parent.minimum === "number") output.minimum = void 0;
601
+ output[key] = typeof parent.minimum === "number" ? parent.minimum : child;
602
+ } else if (key === "exclusiveMaximum" && child === true) {
603
+ const parent = value;
604
+ if (typeof parent.maximum === "number") output.maximum = void 0;
605
+ output[key] = typeof parent.maximum === "number" ? parent.maximum : child;
606
+ } else {
607
+ output[key] = cloneSchema(child, rootId, seen);
608
+ }
609
+ }
610
+ if (output.minimum === void 0) delete output.minimum;
611
+ if (output.maximum === void 0) delete output.maximum;
612
+ return output;
613
+ }
614
+ function createValidator(root, schema) {
615
+ const Validator = root.openapi?.startsWith("3.1") ? Ajv2020 : Ajv;
616
+ const ajv = new Validator({
617
+ allErrors: true,
618
+ strict: false,
619
+ allowUnionTypes: true,
620
+ validateFormats: true
621
+ });
622
+ addFormats(ajv);
623
+ try {
624
+ ajv.addSchema(cloneSchema(root, ROOT_ID), ROOT_ID);
625
+ } catch {
626
+ }
627
+ const normalized = cloneSchema(schema, ROOT_ID);
628
+ return ajv.compile(normalized);
629
+ }
630
+ function validateSchema(root, schema, value) {
631
+ try {
632
+ const validator = createValidator(root, schema);
633
+ const valid = validator(value) === true;
634
+ return { valid, errors: validator.errors ?? [] };
635
+ } catch (error) {
636
+ return {
637
+ valid: false,
638
+ errors: [
639
+ {
640
+ keyword: "schema",
641
+ instancePath: "",
642
+ schemaPath: "",
643
+ params: {},
644
+ message: error instanceof Error ? error.message : "Unable to compile schema"
645
+ }
646
+ ]
647
+ };
648
+ }
649
+ }
650
+ function createFaker(seed) {
651
+ const faker = new Faker({ locale: [en] });
652
+ faker.seed(seed);
653
+ faker.setDefaultRefDate("2020-01-01T00:00:00.000Z");
654
+ return faker;
655
+ }
656
+ function hintedValue(name, type, faker, rng) {
657
+ const normalized = name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().replace(/[^a-z0-9]/g, "");
658
+ if (type === "string") {
659
+ if (normalized === "email" || normalized.endsWith("email"))
660
+ return faker.internet.email().toLowerCase();
661
+ if (normalized === "firstname" || normalized === "givenname") return faker.person.firstName();
662
+ if (normalized === "lastname" || normalized === "familyname" || normalized === "surname")
663
+ return faker.person.lastName();
664
+ if (normalized === "fullname" || normalized === "name" || normalized === "username")
665
+ return normalized === "username" ? faker.internet.username() : faker.person.fullName();
666
+ if (normalized === "phone" || normalized === "phonenumber" || normalized.endsWith("phone"))
667
+ return faker.phone.number();
668
+ if (normalized === "createdat" || normalized === "updatedat" || normalized === "timestamp")
669
+ return faker.date.recent({ days: 10 }).toISOString();
670
+ if (normalized === "avatarurl" || normalized === "imageurl" || normalized === "photo")
671
+ return faker.image.urlLoremFlickr({ category: "portrait" });
672
+ if (normalized === "country") return faker.location.country();
673
+ if (normalized === "city") return faker.location.city();
674
+ if (normalized === "url" || normalized.endsWith("url")) return faker.internet.url();
675
+ if (normalized === "status") return rng.pick(["active", "pending", "inactive"]);
676
+ }
677
+ if (type === "number" || type === "integer") {
678
+ if (normalized === "price" || normalized.endsWith("price") || normalized === "amount")
679
+ return Number(faker.commerce.price({ min: 1, max: 999, dec: 2 }));
680
+ if (normalized === "age") return faker.number.int({ min: 18, max: 80 });
681
+ }
682
+ if (type === "boolean" && (normalized === "active" || normalized === "enabled" || normalized === "verified")) {
683
+ return rng.bool();
684
+ }
685
+ return void 0;
686
+ }
687
+
688
+ // src/generate/schema.ts
689
+ function isRecord3(value) {
690
+ return typeof value === "object" && value !== null && !Array.isArray(value);
691
+ }
692
+ function numberValue(value, fallback) {
693
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
694
+ }
695
+ function stringValue(value, fallback = "") {
696
+ return typeof value === "string" ? value : fallback;
697
+ }
698
+ function getPointer(root, pointer) {
699
+ if (!pointer.startsWith("#/")) return void 0;
700
+ return pointer.slice(2).split("/").reduce((current, part) => {
701
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
702
+ if (Array.isArray(current)) return current[Number(key)];
703
+ if (isRecord3(current)) return current[key];
704
+ return void 0;
705
+ }, root);
706
+ }
707
+ function resolveSchema(schema, context) {
708
+ if (typeof schema.$ref !== "string") return schema;
709
+ const target = getPointer(context.root, schema.$ref);
710
+ if (!isRecord3(target)) {
711
+ context.warnings.push({
712
+ message: `Could not resolve schema reference ${schema.$ref}; using an empty value.`
713
+ });
714
+ return {};
715
+ }
716
+ return target;
717
+ }
718
+ function mergeSchema(left, right) {
719
+ const merged = { ...left, ...right };
720
+ const leftProperties = isRecord3(left.properties) ? left.properties : {};
721
+ const rightProperties = isRecord3(right.properties) ? right.properties : {};
722
+ if (Object.keys(leftProperties).length > 0 || Object.keys(rightProperties).length > 0) {
723
+ merged.properties = { ...leftProperties, ...rightProperties };
724
+ }
725
+ const required = [
726
+ .../* @__PURE__ */ new Set([
727
+ ...Array.isArray(left.required) ? left.required : [],
728
+ ...Array.isArray(right.required) ? right.required : []
729
+ ])
730
+ ];
731
+ if (required.length > 0) merged.required = required;
732
+ for (const key of ["minimum", "exclusiveMinimum"]) {
733
+ const a = left[key];
734
+ const b = right[key];
735
+ if (typeof a === "number" && typeof b === "number") merged[key] = Math.max(a, b);
736
+ }
737
+ for (const key of ["maximum", "exclusiveMaximum"]) {
738
+ const a = left[key];
739
+ const b = right[key];
740
+ if (typeof a === "number" && typeof b === "number") merged[key] = Math.min(a, b);
741
+ }
742
+ if (Array.isArray(left.enum) && Array.isArray(right.enum)) {
743
+ merged.enum = left.enum.filter(
744
+ (value) => right.enum?.some((other) => JSON.stringify(other) === JSON.stringify(value))
745
+ );
746
+ }
747
+ return merged;
748
+ }
749
+ function schemaType(schema, rng) {
750
+ const type = schema.type;
751
+ if (Array.isArray(type)) {
752
+ const alternatives = type.filter(
753
+ (candidate) => typeof candidate === "string"
754
+ );
755
+ return rng.pick(alternatives) ?? "object";
756
+ }
757
+ if (typeof type === "string") return type;
758
+ if (schema.properties || schema.additionalProperties) return "object";
759
+ if (schema.items) return "array";
760
+ return "string";
761
+ }
762
+ function isUnconstrained(schema) {
763
+ return ![
764
+ "enum",
765
+ "const",
766
+ "default",
767
+ "format",
768
+ "pattern",
769
+ "minimum",
770
+ "maximum",
771
+ "exclusiveMinimum",
772
+ "exclusiveMaximum",
773
+ "multipleOf",
774
+ "minLength",
775
+ "maxLength"
776
+ ].some((key) => key in schema);
777
+ }
778
+ function expandCharacterClass(value) {
779
+ let index = 0;
780
+ const chars = [];
781
+ while (index < value.length) {
782
+ const first = value[index] ?? "";
783
+ if (first === "\\") {
784
+ const escaped = value[index + 1];
785
+ if (escaped === "d") chars.push(..."0123456789");
786
+ else if (escaped === "w")
787
+ chars.push(..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_");
788
+ else if (escaped === "s") chars.push(" ");
789
+ else if (escaped) chars.push(escaped);
790
+ index += 2;
791
+ continue;
792
+ }
793
+ const rangeEnd = value[index + 1] === "-" ? value[index + 2] : void 0;
794
+ if (rangeEnd !== void 0) {
795
+ const from = first.codePointAt(0) ?? 0;
796
+ const to = rangeEnd.codePointAt(0) ?? 0;
797
+ if (to < from || to - from > 200) return void 0;
798
+ for (let code = from; code <= to; code += 1) chars.push(String.fromCodePoint(code));
799
+ index += 3;
800
+ continue;
801
+ }
802
+ chars.push(first);
803
+ index += 1;
804
+ }
805
+ return chars.length > 0 ? chars : void 0;
806
+ }
807
+ function generateSimplePattern(pattern, rng) {
808
+ const source = pattern.replace(/^\^/, "").replace(/\$$/, "");
809
+ if (source.includes("|") || source.includes("(?") || source.includes(")")) return void 0;
810
+ let output = "";
811
+ for (let index = 0; index < source.length; ) {
812
+ let choices;
813
+ const char = source[index] ?? "";
814
+ if (char === "[") {
815
+ const end = source.indexOf("]", index + 1);
816
+ if (end < 0 || source[index + 1] === "^") return void 0;
817
+ choices = expandCharacterClass(source.slice(index + 1, end)) ?? [];
818
+ if (choices.length === 0) return void 0;
819
+ index = end + 1;
820
+ } else if (char === "\\") {
821
+ const next = source[index + 1];
822
+ if (next === "d") choices = [..."0123456789"];
823
+ else if (next === "w")
824
+ choices = [..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
825
+ else if (next === "s") choices = [" "];
826
+ else if (next) choices = [next];
827
+ else return void 0;
828
+ index += 2;
829
+ } else if (char === ".") {
830
+ choices = ["a", "b", "x", "7"];
831
+ index += 1;
832
+ } else if ("*+?{}[]()".includes(char)) {
833
+ return void 0;
834
+ } else {
835
+ choices = [char];
836
+ index += 1;
837
+ }
838
+ let minimum = 1;
839
+ let maximum = 1;
840
+ if (source[index] === "{") {
841
+ const match = /^\{(\d+)(?:,(\d*))?\}/.exec(source.slice(index));
842
+ if (!match) return void 0;
843
+ minimum = Number(match[1]);
844
+ maximum = match[2] === void 0 ? minimum : match[2] === "" ? minimum + 3 : Number(match[2]);
845
+ if (maximum < minimum || maximum > 100) return void 0;
846
+ index += match[0].length;
847
+ } else if (source[index] === "+") {
848
+ minimum = 1;
849
+ maximum = 3;
850
+ index += 1;
851
+ } else if (source[index] === "*") {
852
+ minimum = 0;
853
+ maximum = 3;
854
+ index += 1;
855
+ } else if (source[index] === "?") {
856
+ minimum = 0;
857
+ maximum = 1;
858
+ index += 1;
859
+ }
860
+ const count = rng.int(minimum, maximum);
861
+ for (let repeat = 0; repeat < count; repeat += 1) output += rng.pick(choices) ?? "x";
862
+ }
863
+ try {
864
+ return new RegExp(pattern).test(output) ? output : void 0;
865
+ } catch {
866
+ return void 0;
867
+ }
868
+ }
869
+ function formatString(format, faker) {
870
+ if (format === "email") return faker.internet.email().toLowerCase();
871
+ if (format === "uuid") return faker.string.uuid();
872
+ if (format === "date") return faker.date.anytime().toISOString().slice(0, 10);
873
+ if (format === "date-time") return faker.date.anytime().toISOString();
874
+ if (format === "uri" || format === "url") return faker.internet.url();
875
+ if (format === "hostname") return faker.internet.domainName();
876
+ if (format === "ipv4") return faker.internet.ipv4();
877
+ if (format === "ipv6") return faker.internet.ipv6();
878
+ return void 0;
879
+ }
880
+ function fallbackPatternValue(pattern, context) {
881
+ try {
882
+ const expression = new RegExp(pattern);
883
+ const candidates = [
884
+ "a",
885
+ "x",
886
+ "0",
887
+ "test",
888
+ "foo",
889
+ "abc",
890
+ "ab",
891
+ "cd",
892
+ "abc123",
893
+ "2020-01-01",
894
+ context.faker.lorem.word()
895
+ ];
896
+ return candidates.find((candidate) => expression.test(candidate));
897
+ } catch {
898
+ return void 0;
899
+ }
900
+ }
901
+ function constrainedString(schema, context, propertyName) {
902
+ const pattern = stringValue(schema.pattern);
903
+ if (pattern) {
904
+ const matching = generateSimplePattern(pattern, context.rng);
905
+ if (matching !== void 0) return matching;
906
+ context.warnings.push({
907
+ message: `Pattern ${JSON.stringify(pattern)} is too complex for the built-in generator; a fallback value was used.`
908
+ });
909
+ const fallback = fallbackPatternValue(pattern, context);
910
+ if (fallback !== void 0) return fallback;
911
+ }
912
+ const format = stringValue(schema.format);
913
+ let candidate = formatString(format, context.faker);
914
+ const isUnconstrainedSchema = isUnconstrained(schema);
915
+ const hintOption = context.hints;
916
+ if (candidate === void 0 && isUnconstrainedSchema && hintOption !== false) {
917
+ if (isRecord3(hintOption)) {
918
+ const custom = hintOption[propertyName];
919
+ if (Array.isArray(custom)) candidate = String(context.rng.pick(custom) ?? "");
920
+ else if (custom !== void 0) candidate = String(custom);
921
+ }
922
+ candidate ??= hintedValue(propertyName, "string", context.faker, context.rng);
923
+ }
924
+ candidate ??= context.faker.lorem.word();
925
+ const minimum = Math.max(0, Math.floor(numberValue(schema.minLength, 0)));
926
+ const maximum = Math.max(
927
+ minimum,
928
+ Math.floor(numberValue(schema.maxLength, Math.max(80, candidate.length)))
929
+ );
930
+ if (candidate.length < minimum) candidate = candidate.padEnd(minimum, "x");
931
+ if (candidate.length > maximum) candidate = candidate.slice(0, maximum);
932
+ return candidate;
933
+ }
934
+ function generateNumber(schema, context, type, propertyName) {
935
+ if (context.hints !== false && isUnconstrained(schema)) {
936
+ const hintOption = context.hints;
937
+ const custom = isRecord3(hintOption) ? hintOption[propertyName] : void 0;
938
+ const hinted = custom !== void 0 ? Number(Array.isArray(custom) ? context.rng.pick(custom) : custom) : hintedValue(propertyName, type, context.faker, context.rng);
939
+ if (typeof hinted === "number" && Number.isFinite(hinted))
940
+ return type === "integer" ? Math.round(hinted) : hinted;
941
+ }
942
+ const minimumValue = schema.minimum;
943
+ const maximumValue = schema.maximum;
944
+ let minimum = typeof minimumValue === "number" ? minimumValue : Math.min(-100, numberValue(maximumValue, 100) - 200);
945
+ let maximum = typeof maximumValue === "number" ? maximumValue : Math.max(100, minimum + 200);
946
+ if (schema.exclusiveMinimum === true && typeof minimumValue === "number")
947
+ minimum += type === "integer" ? 1 : 1e-6;
948
+ else if (typeof schema.exclusiveMinimum === "number")
949
+ minimum = Math.max(
950
+ minimum,
951
+ schema.exclusiveMinimum + (type === "integer" ? 1 : Number.EPSILON)
952
+ );
953
+ if (schema.exclusiveMaximum === true && typeof maximumValue === "number")
954
+ maximum -= type === "integer" ? 1 : 1e-6;
955
+ else if (typeof schema.exclusiveMaximum === "number")
956
+ maximum = Math.min(
957
+ maximum,
958
+ schema.exclusiveMaximum - (type === "integer" ? 1 : Number.EPSILON)
959
+ );
960
+ if (minimum > maximum) minimum = maximum;
961
+ const multipleOf = numberValue(schema.multipleOf, 0);
962
+ if (type === "integer") {
963
+ const low = Math.ceil(minimum);
964
+ const high = Math.floor(maximum);
965
+ if (multipleOf > 0) {
966
+ const first = Math.ceil(low / multipleOf) * multipleOf;
967
+ const last = Math.floor(high / multipleOf) * multipleOf;
968
+ if (first <= last)
969
+ return Math.round(context.rng.int(first / multipleOf, last / multipleOf) * multipleOf);
970
+ return Math.round(first);
971
+ }
972
+ return context.rng.int(low, high);
973
+ }
974
+ let number = context.rng.float(minimum, maximum);
975
+ if (multipleOf > 0) number = Math.round(number / multipleOf) * multipleOf;
976
+ number = Math.min(maximum, Math.max(minimum, number));
977
+ return Number(number.toPrecision(8));
978
+ }
979
+ function generateArray(schema, context, depth, propertyName) {
980
+ const minimum = Math.max(0, Math.floor(numberValue(schema.minItems, context.minArrayItems)));
981
+ const maximum = Math.max(
982
+ minimum,
983
+ Math.floor(numberValue(schema.maxItems, context.maxArrayItems))
984
+ );
985
+ const count = context.rng.int(minimum, maximum);
986
+ const itemSchema = isRecord3(schema.items) ? schema.items : {};
987
+ const values = [];
988
+ const unique = schema.uniqueItems === true;
989
+ let attempts = 0;
990
+ while (values.length < count && attempts < Math.max(count * 10, 10)) {
991
+ attempts += 1;
992
+ const value = generateNode(itemSchema, context, depth + 1, `${propertyName}Item`);
993
+ if (!unique || !values.some((existing) => JSON.stringify(existing) === JSON.stringify(value)))
994
+ values.push(value);
995
+ }
996
+ return values;
997
+ }
998
+ function generateObject(schema, context, depth) {
999
+ const properties = isRecord3(schema.properties) ? schema.properties : {};
1000
+ const required = new Set(
1001
+ Array.isArray(schema.required) ? schema.required.filter((v) => typeof v === "string") : []
1002
+ );
1003
+ const result = {};
1004
+ for (const [name, childSchema] of Object.entries(properties)) {
1005
+ if (context.mode === "response" && childSchema.writeOnly === true) continue;
1006
+ if (context.mode === "request" && childSchema.readOnly === true) continue;
1007
+ const isRequired = required.has(name);
1008
+ if (!isRequired && depth >= context.maxDepth) continue;
1009
+ if (!isRequired && !context.rng.bool(0.72)) continue;
1010
+ result[name] = generateNode(childSchema, context, depth + 1, name);
1011
+ }
1012
+ if (schema.additionalProperties && isRecord3(schema.additionalProperties)) {
1013
+ const extensionName = properties.extension ? "extra" : "extension";
1014
+ result[extensionName] = generateNode(
1015
+ schema.additionalProperties,
1016
+ context,
1017
+ depth + 1,
1018
+ extensionName
1019
+ );
1020
+ }
1021
+ return result;
1022
+ }
1023
+ function discriminatorValue(schema, branch, context) {
1024
+ const discriminator = isRecord3(schema.discriminator) ? schema.discriminator : void 0;
1025
+ const propertyName = typeof discriminator?.propertyName === "string" ? discriminator.propertyName : void 0;
1026
+ if (!propertyName) return void 0;
1027
+ const mapping = isRecord3(discriminator?.mapping) ? discriminator.mapping : void 0;
1028
+ if (mapping) {
1029
+ const match = Object.entries(mapping).find(
1030
+ ([, ref]) => typeof ref === "string" && ref === branch.$ref
1031
+ );
1032
+ if (match) return { name: propertyName, value: match[0] };
1033
+ }
1034
+ const resolved = resolveSchema(branch, context);
1035
+ const properties = isRecord3(resolved.properties) ? resolved.properties : {};
1036
+ const propertySchema = isRecord3(properties[propertyName]) ? properties[propertyName] : void 0;
1037
+ if (propertySchema && "const" in propertySchema)
1038
+ return { name: propertyName, value: propertySchema.const };
1039
+ if (Array.isArray(propertySchema?.enum))
1040
+ return { name: propertyName, value: propertySchema.enum[0] };
1041
+ if (typeof branch.$ref === "string")
1042
+ return { name: propertyName, value: branch.$ref.split("/").pop() ?? "variant" };
1043
+ return void 0;
1044
+ }
1045
+ function chooseCompositionBranch(schema, context) {
1046
+ const branches = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : void 0;
1047
+ if (!branches || branches.length === 0) return void 0;
1048
+ const usable = branches.filter(isRecord3);
1049
+ if (usable.length === 0) return void 0;
1050
+ const discriminator = isRecord3(schema.discriminator) ? schema.discriminator : void 0;
1051
+ if (typeof discriminator?.propertyName === "string" && isRecord3(discriminator.mapping)) {
1052
+ const mappedRefs = Object.values(discriminator.mapping).filter(
1053
+ (value) => typeof value === "string"
1054
+ );
1055
+ const selectedRef = context.rng.pick(mappedRefs);
1056
+ const found = usable.find((branch) => branch.$ref === selectedRef);
1057
+ if (found) return found;
1058
+ }
1059
+ return context.rng.pick(usable);
1060
+ }
1061
+ function generateNode(rawSchema, context, depth, propertyName) {
1062
+ if (depth > context.maxDepth + 8) {
1063
+ throw new Error("Recursive schema has no finite value within the generation depth limit.");
1064
+ }
1065
+ let schema = resolveSchema(rawSchema, context);
1066
+ if (rawSchema.$ref && context.refTrail.has(rawSchema.$ref)) {
1067
+ if (depth >= context.maxDepth) {
1068
+ const resolved = resolveSchema(rawSchema, context);
1069
+ if (resolved.nullable === true || Array.isArray(resolved.type) && resolved.type.includes("null"))
1070
+ return null;
1071
+ if (resolved.default !== void 0) return resolved.default;
1072
+ if (resolved.const !== void 0) return resolved.const;
1073
+ const type = schemaType(resolved, context.rng);
1074
+ if (type === "array")
1075
+ return numberValue(resolved.minItems, 0) === 0 ? [] : generateArray(resolved, context, depth, propertyName);
1076
+ if (type === "object") return generateObject(resolved, context, depth);
1077
+ if (type === "integer" || type === "number")
1078
+ return generateNumber(resolved, context, type, propertyName);
1079
+ if (type === "boolean") return context.rng.bool();
1080
+ if (type === "string") return constrainedString(resolved, context, propertyName);
1081
+ return null;
1082
+ }
1083
+ }
1084
+ if (rawSchema.$ref) context.refTrail.add(rawSchema.$ref);
1085
+ try {
1086
+ if ("const" in schema) return schema.const;
1087
+ if (schema.default !== void 0) return schema.default;
1088
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return context.rng.pick(schema.enum);
1089
+ if (schema.nullable === true && context.rng.bool(0.12)) return null;
1090
+ const allOf = Array.isArray(schema.allOf) ? schema.allOf.filter(isRecord3) : [];
1091
+ if (allOf.length > 0) {
1092
+ let merged = { ...schema };
1093
+ delete merged.allOf;
1094
+ for (const item of allOf) merged = mergeSchema(merged, resolveSchema(item, context));
1095
+ schema = merged;
1096
+ }
1097
+ const branch = chooseCompositionBranch(schema, context);
1098
+ if (branch) {
1099
+ const generated = generateNode(branch, context, depth + 1, propertyName);
1100
+ const disc = discriminatorValue(schema, branch, context);
1101
+ const base = { ...schema };
1102
+ delete base.oneOf;
1103
+ delete base.anyOf;
1104
+ delete base.discriminator;
1105
+ const generatedBase = generateNode(base, context, depth, propertyName);
1106
+ if (isRecord3(generated)) {
1107
+ const combined = { ...isRecord3(generatedBase) ? generatedBase : {}, ...generated };
1108
+ if (disc) combined[disc.name] = disc.value;
1109
+ return combined;
1110
+ }
1111
+ return generated;
1112
+ }
1113
+ const type = schemaType(schema, context.rng);
1114
+ if (type === "null") return null;
1115
+ if (type === "object") return generateObject(schema, context, depth);
1116
+ if (type === "array") return generateArray(schema, context, depth, propertyName);
1117
+ if (type === "integer" || type === "number")
1118
+ return generateNumber(schema, context, type, propertyName);
1119
+ if (type === "boolean") return context.rng.bool();
1120
+ if (type === "string") {
1121
+ const typeArray = Array.isArray(schema.type) ? schema.type : [];
1122
+ if (typeArray.includes("null") && context.rng.bool(0.08)) return null;
1123
+ return constrainedString(schema, context, propertyName);
1124
+ }
1125
+ return null;
1126
+ } finally {
1127
+ if (rawSchema.$ref) context.refTrail.delete(rawSchema.$ref);
1128
+ }
1129
+ }
1130
+ function createContext(options, seed) {
1131
+ return {
1132
+ root: options.root ?? {},
1133
+ rng: new SeededRandom(seed),
1134
+ faker: createFaker(seed),
1135
+ maxDepth: options.maxDepth ?? 3,
1136
+ minArrayItems: options.minArrayItems ?? 1,
1137
+ maxArrayItems: options.maxArrayItems ?? 3,
1138
+ hints: options.hints ?? true,
1139
+ mode: options.mode ?? "response",
1140
+ warnings: [],
1141
+ refTrail: /* @__PURE__ */ new Set()
1142
+ };
1143
+ }
1144
+ function stripModeOnlyFields(value, field, seen = /* @__PURE__ */ new WeakMap()) {
1145
+ if (Array.isArray(value)) return value.map((item) => stripModeOnlyFields(item, field, seen));
1146
+ if (!isRecord3(value)) return value;
1147
+ const known = seen.get(value);
1148
+ if (known) return known;
1149
+ const output = {};
1150
+ seen.set(value, output);
1151
+ for (const [key, child] of Object.entries(value)) {
1152
+ if (key === "properties" && isRecord3(child)) {
1153
+ const properties = {};
1154
+ for (const [name, schemaValue] of Object.entries(child)) {
1155
+ if (isRecord3(schemaValue) && schemaValue[field] === true) continue;
1156
+ properties[name] = stripModeOnlyFields(schemaValue, field, seen);
1157
+ }
1158
+ output[key] = properties;
1159
+ } else if (key === "required" && Array.isArray(child)) {
1160
+ const properties = isRecord3(value.properties) ? value.properties : {};
1161
+ output[key] = child.filter(
1162
+ (name) => typeof name !== "string" || !isRecord3(properties[name]) || properties[name]?.[field] !== true
1163
+ );
1164
+ } else {
1165
+ output[key] = stripModeOnlyFields(child, field, seen);
1166
+ }
1167
+ }
1168
+ return output;
1169
+ }
1170
+ function generateFromSchema(schema, options = {}) {
1171
+ const baseSeed = options.seed ?? 0;
1172
+ const mode = options.mode ?? "response";
1173
+ const field = mode === "response" ? "writeOnly" : "readOnly";
1174
+ const validationRoot = stripModeOnlyFields(options.root ?? {}, field);
1175
+ const validationSchema = stripModeOnlyFields(schema, field);
1176
+ let last;
1177
+ const warnings = [];
1178
+ for (let attempt = 0; attempt < 12; attempt += 1) {
1179
+ const seed = deriveSeed(baseSeed, "generate", attempt);
1180
+ const context = createContext(options, seed);
1181
+ last = generateNode(schema, context, 0, "");
1182
+ warnings.push(...context.warnings);
1183
+ const validation = validateSchema(validationRoot, validationSchema, last);
1184
+ if (validation.valid) {
1185
+ return { value: last, warnings: dedupeWarnings(warnings) };
1186
+ }
1187
+ }
1188
+ const reason = warnings.map((warning) => warning.message).join("; ");
1189
+ throw new Error(
1190
+ `Mockline could not generate a value satisfying its schema.${reason ? ` ${reason}` : ""}`
1191
+ );
1192
+ }
1193
+ function generateValue(schema, options = {}) {
1194
+ return generateFromSchema(schema, options).value;
1195
+ }
1196
+ function dedupeWarnings(warnings) {
1197
+ const unique = new Map(warnings.map((warning) => [warning.message, warning]));
1198
+ return [...unique.values()];
1199
+ }
1200
+
1201
+ // src/select.ts
1202
+ function isRecord4(value) {
1203
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1204
+ }
1205
+ function headerValue(headers, name) {
1206
+ const value = headers[name.toLowerCase()] ?? headers[name];
1207
+ return Array.isArray(value) ? value.join(",") : value;
1208
+ }
1209
+ function parseControls(input) {
1210
+ const allow = input.config.controlHeaders.allow;
1211
+ if (!allow) return { forceError: false, active: false };
1212
+ const prefer = headerValue(input.headers, "prefer") ?? "";
1213
+ const statusMatch = /(?:^|[,;]\s*)code\s*=\s*(\d{3})/i.exec(prefer);
1214
+ const exampleMatch = /(?:^|[,;]\s*)example\s*=\s*"?([^,;"\s]+)"?/i.exec(prefer);
1215
+ const headerStatus = Number(headerValue(input.headers, "x-mockline-status"));
1216
+ const status = Number.isInteger(headerStatus) && headerStatus >= 100 && headerStatus <= 599 ? headerStatus : statusMatch?.[1] ? Number(statusMatch[1]) : void 0;
1217
+ const delayValue = Number(headerValue(input.headers, "x-mockline-delay"));
1218
+ const delay = Number.isFinite(delayValue) && delayValue >= 0 ? delayValue : void 0;
1219
+ const forceError = /^(true|1|yes)$/i.test(headerValue(input.headers, "x-mockline-error") ?? "");
1220
+ const example = exampleMatch?.[1];
1221
+ return {
1222
+ status,
1223
+ example,
1224
+ delay,
1225
+ forceError,
1226
+ active: status !== void 0 || example !== void 0 || forceError
1227
+ };
1228
+ }
1229
+ function responseForStatus(responses, requestedStatus) {
1230
+ const standardStatus = defaultStatus(responses);
1231
+ const status = requestedStatus ?? (standardStatus === "default" ? 200 : Number(standardStatus));
1232
+ return {
1233
+ status,
1234
+ response: responses?.[String(status)] ?? responses?.default ?? (requestedStatus === void 0 ? responses?.[standardStatus] : void 0)
1235
+ };
1236
+ }
1237
+ function jsonMediaType(media) {
1238
+ const normalized = media.split(";")[0]?.trim().toLowerCase() ?? "";
1239
+ return normalized === "application/json" || normalized.endsWith("+json") || normalized === "application/problem+json";
1240
+ }
1241
+ function parseAccept(header) {
1242
+ if (!header || header.trim() === "") return [{ value: "*/*", q: 1, index: 0 }];
1243
+ return header.split(",").map((part, index) => {
1244
+ const [value = "*/*", ...parameters] = part.trim().split(";");
1245
+ const qValue = parameters.find((parameter) => parameter.trim().startsWith("q="))?.trim().slice(2);
1246
+ const parsedQ = qValue === void 0 ? 1 : Number(qValue);
1247
+ return {
1248
+ value: value.trim().toLowerCase(),
1249
+ q: Number.isFinite(parsedQ) ? parsedQ : 0,
1250
+ index
1251
+ };
1252
+ }).filter((item) => item.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
1253
+ }
1254
+ function mediaMatches(accept, media) {
1255
+ const accepted = accept.split(";")[0]?.trim().toLowerCase() ?? "";
1256
+ const offered = media.split(";")[0]?.trim().toLowerCase() ?? "";
1257
+ if (accepted === "*/*" || accepted === offered) return true;
1258
+ const [acceptedType, acceptedSubtype] = accepted.split("/");
1259
+ const [offeredType, offeredSubtype] = offered.split("/");
1260
+ if (acceptedType === offeredType && acceptedSubtype === "*") return true;
1261
+ if (acceptedType === "*" && Boolean(acceptedSubtype)) return false;
1262
+ if (acceptedSubtype?.startsWith("*+"))
1263
+ return offeredSubtype?.endsWith(acceptedSubtype.slice(1)) ?? false;
1264
+ return false;
1265
+ }
1266
+ function negotiateMedia(content, accept) {
1267
+ const available = Object.keys(content);
1268
+ if (available.length === 0) return void 0;
1269
+ const preferences = parseAccept(accept);
1270
+ for (const preference of preferences) {
1271
+ const compatible = available.filter((media) => mediaMatches(preference.value, media));
1272
+ if (compatible.length > 0) {
1273
+ return compatible.find(jsonMediaType) ?? compatible[0];
1274
+ }
1275
+ }
1276
+ return void 0;
1277
+ }
1278
+ function exampleValue(value) {
1279
+ if (isRecord4(value) && "value" in value) return value.value;
1280
+ return value;
1281
+ }
1282
+ function findExample(media, preferredName) {
1283
+ const examples = isRecord4(media.examples) ? media.examples : void 0;
1284
+ if (examples) {
1285
+ if (preferredName && preferredName in examples)
1286
+ return { found: true, value: exampleValue(examples[preferredName]) };
1287
+ const first = Object.values(examples)[0];
1288
+ if (first !== void 0) return { found: true, value: exampleValue(first) };
1289
+ }
1290
+ if ("example" in media) return { found: true, value: media.example };
1291
+ const schema = isRecord4(media.schema) ? media.schema : void 0;
1292
+ if (schema) {
1293
+ if ("example" in schema) return { found: true, value: schema.example };
1294
+ if (Array.isArray(schema.examples) && schema.examples.length > 0)
1295
+ return { found: true, value: schema.examples[0] };
1296
+ }
1297
+ return { found: false };
1298
+ }
1299
+ function matchesCondition(condition, input) {
1300
+ let actual;
1301
+ if (condition.location === "query")
1302
+ actual = input.url.searchParams.get(condition.name) ?? void 0;
1303
+ else if (condition.location === "header")
1304
+ actual = headerValue(input.headers, condition.name.toLowerCase());
1305
+ else {
1306
+ const path2 = condition.name.startsWith("/") ? condition.name.slice(1).split("/") : condition.name.split(".");
1307
+ actual = path2.reduce((current, part) => {
1308
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
1309
+ return isRecord4(current) ? current[key] : void 0;
1310
+ }, input.body);
1311
+ }
1312
+ const operation = condition.op ?? "eq";
1313
+ if (operation === "eq") return String(actual) === String(condition.value);
1314
+ if (operation === "contains") {
1315
+ if (Array.isArray(actual))
1316
+ return actual.some((item) => String(item).includes(String(condition.value)));
1317
+ return typeof actual === "string" && actual.includes(String(condition.value));
1318
+ }
1319
+ try {
1320
+ return typeof actual === "string" && new RegExp(String(condition.value)).test(actual);
1321
+ } catch {
1322
+ return false;
1323
+ }
1324
+ }
1325
+ function overrideKey(input) {
1326
+ const entries = Object.entries(input.config.overrides);
1327
+ return entries.find(([key]) => key === input.route.operationId)?.[0] ?? entries.find(
1328
+ ([key]) => key.toUpperCase() === `${input.route.method} ${input.route.path}`.toUpperCase()
1329
+ )?.[0];
1330
+ }
1331
+ function selectOverride(override, key, input) {
1332
+ if (override.rules?.length) {
1333
+ const matching = override.rules.find((rule) => matchesCondition(rule.when, input));
1334
+ if (matching) return { value: matching.response };
1335
+ }
1336
+ if (override.when && override.response && matchesCondition(override.when, input))
1337
+ return { value: override.response };
1338
+ if (override.sequence?.length) {
1339
+ const index = input.sequenceIndexes?.get(key) ?? 0;
1340
+ const selectedIndex = override.loop ? index % override.sequence.length : Math.min(index, override.sequence.length - 1);
1341
+ const selected = override.sequence[selectedIndex];
1342
+ if (selected) return { value: selected, sequenceKey: key };
1343
+ }
1344
+ if (!override.when && !override.rules?.length) {
1345
+ const fixed = {};
1346
+ if (override.status !== void 0) fixed.status = override.status;
1347
+ if ("body" in override) fixed.body = override.body;
1348
+ if (override.headers) fixed.headers = override.headers;
1349
+ if (override.delay !== void 0) fixed.delay = override.delay;
1350
+ return { value: fixed };
1351
+ }
1352
+ return void 0;
1353
+ }
1354
+ function calculateDelay(config, overrideDelay, controlDelay, seed, requestCounter) {
1355
+ if (controlDelay !== void 0) return controlDelay;
1356
+ if (overrideDelay !== void 0) return overrideDelay;
1357
+ const latency = config.latency;
1358
+ const rng = new SeededRandom(deriveSeed(seed, "latency", requestCounter));
1359
+ if (typeof latency === "number") return latency > 0 ? latency : void 0;
1360
+ if (Array.isArray(latency)) return rng.int(latency[0], latency[1]);
1361
+ if ("fixed" in latency && latency.fixed !== void 0)
1362
+ return latency.fixed > 0 ? latency.fixed : void 0;
1363
+ if ("min" in latency && latency.min !== void 0 && latency.max !== void 0) {
1364
+ if (latency.distribution === "normal") {
1365
+ const u1 = Math.max(rng.next(), Number.EPSILON);
1366
+ const u2 = rng.next();
1367
+ const normal = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
1368
+ const midpoint = (latency.min + latency.max) / 2;
1369
+ const spread = (latency.max - latency.min) / 6;
1370
+ return Math.max(latency.min, Math.min(latency.max, Math.round(midpoint + normal * spread)));
1371
+ }
1372
+ return rng.int(latency.min, latency.max);
1373
+ }
1374
+ return void 0;
1375
+ }
1376
+ function injectedFailure(status, seed, input, force) {
1377
+ const rate = input.config.errors.perOperationRates[input.route.operationId] ?? input.config.errors.rate;
1378
+ const rng = new SeededRandom(deriveSeed(seed, "failure", input.requestCounter ?? 0));
1379
+ if (!force && rng.next() >= rate) return void 0;
1380
+ const failureStatus = force ? input.config.errors.statuses[0] ?? 500 : rng.pick(input.config.errors.statuses) ?? 500;
1381
+ return {
1382
+ status: failureStatus,
1383
+ body: problem(
1384
+ failureStatus,
1385
+ failureStatus >= 500 ? "Mock upstream failure" : "Mock error",
1386
+ "A failure was injected by Mockline.",
1387
+ { originalStatus: status }
1388
+ ),
1389
+ headers: { "content-type": "application/problem+json" },
1390
+ source: "generated"
1391
+ };
1392
+ }
1393
+ function responseWithStatusAndMedia(input, requestedStatus, exampleName, forceSelection, seed) {
1394
+ const warnings = [];
1395
+ const responses = input.route.operation.responses;
1396
+ const selected = responseForStatus(responses, requestedStatus);
1397
+ const status = selected.status;
1398
+ const responseDefinition = selected.response;
1399
+ const content = responseDefinition?.content ?? {};
1400
+ const mediaType = negotiateMedia(content, headerValue(input.headers, "accept"));
1401
+ if (Object.keys(content).length > 0 && mediaType === void 0) {
1402
+ return {
1403
+ response: {
1404
+ status: 406,
1405
+ body: problem(
1406
+ 406,
1407
+ "Not Acceptable",
1408
+ "The requested Accept header does not match any response media type declared by this operation."
1409
+ ),
1410
+ headers: { "content-type": "application/problem+json" },
1411
+ source: "generated"
1412
+ },
1413
+ warnings
1414
+ };
1415
+ }
1416
+ const media = mediaType && isRecord4(content[mediaType]) ? content[mediaType] : void 0;
1417
+ const preferredExample = exampleName;
1418
+ const found = media ? findExample(media, preferredExample) : { found: false };
1419
+ const allowExample = input.config.prefer === "example" || forceSelection || Boolean(exampleName);
1420
+ if (found.found && allowExample) {
1421
+ const exampleHeaders = mediaType ? { "content-type": mediaType } : void 0;
1422
+ return {
1423
+ response: { status, body: found.value, headers: exampleHeaders, source: "example" },
1424
+ warnings
1425
+ };
1426
+ }
1427
+ if (!media) return { response: { status, source: "generated" }, warnings };
1428
+ if (mediaType && !jsonMediaType(mediaType)) {
1429
+ if (found.found)
1430
+ return {
1431
+ response: {
1432
+ status,
1433
+ body: found.value,
1434
+ headers: { "content-type": mediaType },
1435
+ source: "example"
1436
+ },
1437
+ warnings
1438
+ };
1439
+ const body = problem(
1440
+ 501,
1441
+ "Not Implemented",
1442
+ `Mockline cannot synthesize ${mediaType} responses. Add an example to the OpenAPI document.`
1443
+ );
1444
+ return {
1445
+ response: {
1446
+ status: 501,
1447
+ body,
1448
+ headers: { "content-type": "application/problem+json" },
1449
+ source: "generated"
1450
+ },
1451
+ warnings
1452
+ };
1453
+ }
1454
+ const schema = isRecord4(media.schema) ? media.schema : void 0;
1455
+ if (!schema)
1456
+ return {
1457
+ response: {
1458
+ status,
1459
+ headers: mediaType ? { "content-type": mediaType } : void 0,
1460
+ source: "generated"
1461
+ },
1462
+ warnings
1463
+ };
1464
+ const generated = generateFromSchema(schema, {
1465
+ seed,
1466
+ root: input.root,
1467
+ hints: input.config.generation.hints,
1468
+ maxDepth: input.config.generation.maxDepth,
1469
+ minArrayItems: input.config.generation.minArrayItems,
1470
+ maxArrayItems: input.config.generation.maxArrayItems,
1471
+ mode: "response"
1472
+ });
1473
+ warnings.push(...generated.warnings.map((warning) => warning.message));
1474
+ return {
1475
+ response: {
1476
+ status,
1477
+ body: generated.value,
1478
+ headers: mediaType ? { "content-type": mediaType } : void 0,
1479
+ source: "generated"
1480
+ },
1481
+ warnings
1482
+ };
1483
+ }
1484
+ function applyAcceptNegotiation(input, response) {
1485
+ const responses = input.route.operation.responses;
1486
+ const definition = responses?.[String(response.status)] ?? responses?.default;
1487
+ const content = definition?.content;
1488
+ if (!content || Object.keys(content).length === 0 || response.status === 406) return response;
1489
+ const mediaType = negotiateMedia(content, headerValue(input.headers, "accept"));
1490
+ if (!mediaType) {
1491
+ return {
1492
+ status: 406,
1493
+ body: problem(
1494
+ 406,
1495
+ "Not Acceptable",
1496
+ "The requested Accept header does not match any response media type declared by this operation."
1497
+ ),
1498
+ headers: { "content-type": "application/problem+json" },
1499
+ source: "generated"
1500
+ };
1501
+ }
1502
+ return { ...response, headers: { ...response.headers, "content-type": mediaType } };
1503
+ }
1504
+ function selectResponse(input) {
1505
+ const controls = parseControls(input);
1506
+ const baseSeed = input.config.seed ?? 0;
1507
+ const counter = input.config.generation.counter ? input.requestCounter ?? 0 : 0;
1508
+ const seed = deriveSeed(baseSeed, input.route.method, input.route.path, counter);
1509
+ let response;
1510
+ let warnings = [];
1511
+ let sequenceKey;
1512
+ let overrideDelay;
1513
+ const controlSelection = controls.status !== void 0 || controls.example !== void 0 || controls.forceError;
1514
+ if (controlSelection) {
1515
+ const selected = responseWithStatusAndMedia(
1516
+ input,
1517
+ controls.status,
1518
+ controls.example,
1519
+ true,
1520
+ seed
1521
+ );
1522
+ response = selected.response;
1523
+ warnings = selected.warnings;
1524
+ } else {
1525
+ const key = overrideKey(input);
1526
+ const override = key ? input.config.overrides[key] : void 0;
1527
+ const chosenOverride = override && key ? selectOverride(override, key, input) : void 0;
1528
+ if (chosenOverride) {
1529
+ const value = chosenOverride.value;
1530
+ sequenceKey = chosenOverride.sequenceKey;
1531
+ overrideDelay = value.delay;
1532
+ const operationStatus = defaultStatus(input.route.operation.responses);
1533
+ const fallbackStatus = operationStatus === "default" ? 200 : Number(operationStatus);
1534
+ response = {
1535
+ status: value.status ?? fallbackStatus,
1536
+ ...value.body !== void 0 ? { body: value.body } : {},
1537
+ ...value.headers ? { headers: value.headers } : {},
1538
+ ...value.delay !== void 0 ? { delay: value.delay } : {},
1539
+ source: "override"
1540
+ };
1541
+ } else {
1542
+ const failurePending = injectedFailure(200, seed, input, false);
1543
+ const acceptsState = applyAcceptNegotiation(input, {
1544
+ status: Number(defaultStatus(input.route.operation.responses)) || 200,
1545
+ source: "state"
1546
+ }).status !== 406;
1547
+ const stateResponse = !failurePending && acceptsState ? typeof input.stateResponse === "function" ? input.stateResponse() : input.stateResponse : void 0;
1548
+ if (stateResponse) response = stateResponse;
1549
+ else {
1550
+ const selected = responseWithStatusAndMedia(input, void 0, void 0, false, seed);
1551
+ response = selected.response;
1552
+ warnings = selected.warnings;
1553
+ }
1554
+ }
1555
+ }
1556
+ const failure = injectedFailure(response.status, seed, input, controls.forceError);
1557
+ if (failure) response = failure;
1558
+ response = applyAcceptNegotiation(input, response);
1559
+ const delay = calculateDelay(
1560
+ input.config,
1561
+ overrideDelay ?? response.delay,
1562
+ controls.delay,
1563
+ seed,
1564
+ input.requestCounter ?? 0
1565
+ );
1566
+ if (delay !== void 0) response = { ...response, delay };
1567
+ return { response, warnings, ...sequenceKey ? { sequenceKey } : {} };
1568
+ }
1569
+ function redactHeaders(headers, extraRedacted) {
1570
+ const redacted = /* @__PURE__ */ new Set([
1571
+ "authorization",
1572
+ "cookie",
1573
+ "set-cookie",
1574
+ "proxy-authorization",
1575
+ "x-api-key",
1576
+ "api-key",
1577
+ "x-auth-token",
1578
+ "x-access-token",
1579
+ ...extraRedacted.map((name) => name.toLowerCase())
1580
+ ]);
1581
+ const result = {};
1582
+ for (const [name, value] of Object.entries(headers)) {
1583
+ if (value === void 0) continue;
1584
+ result[name] = redacted.has(name.toLowerCase()) ? "[REDACTED]" : value;
1585
+ }
1586
+ return result;
1587
+ }
1588
+ function cloneRecord(record) {
1589
+ return structuredClone(record);
1590
+ }
1591
+ var RequestRecorder = class {
1592
+ buffer = [];
1593
+ maxRequests;
1594
+ file;
1595
+ redact;
1596
+ clock;
1597
+ nextId = 1;
1598
+ writeQueue = Promise.resolve();
1599
+ constructor(config, clock = () => /* @__PURE__ */ new Date()) {
1600
+ this.maxRequests = config.recording.maxRequests;
1601
+ this.file = config.recording.file ? resolve(config.recording.file) : void 0;
1602
+ this.redact = config.recording.redactHeaders;
1603
+ this.clock = clock;
1604
+ }
1605
+ record(input) {
1606
+ const entry = {
1607
+ id: String(this.nextId++),
1608
+ timestamp: this.clock().toISOString(),
1609
+ method: input.method,
1610
+ url: input.url,
1611
+ ...input.operationId ? { operationId: input.operationId } : {},
1612
+ ...input.matchedPath ? { matchedPath: input.matchedPath } : {},
1613
+ headers: redactHeaders(input.headers, this.redact),
1614
+ ...input.body !== void 0 ? { body: input.body } : {},
1615
+ responseStatus: input.responseStatus,
1616
+ responseSource: input.responseSource,
1617
+ durationMs: input.durationMs,
1618
+ validationErrors: [...input.validationErrors]
1619
+ };
1620
+ if (this.buffer.length >= this.maxRequests) this.buffer.shift();
1621
+ this.buffer.push(entry);
1622
+ if (this.file) {
1623
+ this.writeQueue = this.writeQueue.then(async () => {
1624
+ await mkdir(dirname(this.file), { recursive: true });
1625
+ await appendFile(this.file, `${JSON.stringify(entry)}
1626
+ `, "utf8");
1627
+ }).catch((error) => {
1628
+ console.error(
1629
+ `mockline: could not append request record: ${error instanceof Error ? error.message : String(error)}`
1630
+ );
1631
+ });
1632
+ }
1633
+ return cloneRecord(entry);
1634
+ }
1635
+ setConfig(config) {
1636
+ this.maxRequests = config.recording.maxRequests;
1637
+ this.file = config.recording.file ? resolve(config.recording.file) : void 0;
1638
+ this.redact = config.recording.redactHeaders;
1639
+ while (this.buffer.length > this.maxRequests) this.buffer.shift();
1640
+ }
1641
+ list(filter = {}) {
1642
+ const since = filter.since ? new Date(filter.since).getTime() : void 0;
1643
+ let entries = this.buffer.filter((entry) => {
1644
+ if (filter.method && entry.method.toUpperCase() !== filter.method.toUpperCase()) return false;
1645
+ if (filter.path && !entry.url.includes(filter.path)) return false;
1646
+ if (filter.status !== void 0 && entry.responseStatus !== filter.status) return false;
1647
+ if (filter.operationId && entry.operationId !== filter.operationId) return false;
1648
+ if (since !== void 0 && Number.isFinite(since) && new Date(entry.timestamp).getTime() < since)
1649
+ return false;
1650
+ return true;
1651
+ });
1652
+ if (filter.limit !== void 0) entries = filter.limit <= 0 ? [] : entries.slice(-filter.limit);
1653
+ return entries.map(cloneRecord);
1654
+ }
1655
+ get(id) {
1656
+ const entry = this.buffer.find((request) => request.id === id);
1657
+ return entry ? cloneRecord(entry) : void 0;
1658
+ }
1659
+ clear() {
1660
+ this.buffer.length = 0;
1661
+ }
1662
+ async flush() {
1663
+ await this.writeQueue;
1664
+ }
1665
+ };
1666
+
1667
+ // src/inspector.ts
1668
+ function json(status, body) {
1669
+ return { status, body, headers: { "content-type": "application/json; charset=utf-8" } };
1670
+ }
1671
+ function problemResult(status, title, detail) {
1672
+ return {
1673
+ status,
1674
+ body: problem(status, title, detail),
1675
+ headers: { "content-type": "application/problem+json; charset=utf-8" }
1676
+ };
1677
+ }
1678
+ function redactConfig(config) {
1679
+ const copy = structuredClone(config);
1680
+ const proxy = copy.proxy;
1681
+ if (typeof proxy === "object" && proxy !== null && !Array.isArray(proxy)) {
1682
+ proxy.url = proxy.url ? "[REDACTED UPSTREAM URL]" : void 0;
1683
+ proxy.recordFile = proxy.recordFile ? "[PATH REDACTED]" : void 0;
1684
+ }
1685
+ const recording = copy.recording;
1686
+ if (typeof recording === "object" && recording !== null && !Array.isArray(recording) && recording.file) {
1687
+ recording.file = "[PATH REDACTED]";
1688
+ }
1689
+ return copy;
1690
+ }
1691
+ function parseInteger(searchParams, key) {
1692
+ const value = searchParams.get(key);
1693
+ if (value === null) return void 0;
1694
+ const parsed = Number(value);
1695
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
1696
+ }
1697
+ function requestFilter(url) {
1698
+ const status = parseInteger(url.searchParams, "status");
1699
+ const limit = parseInteger(url.searchParams, "limit");
1700
+ if (status === null || limit === null || status !== void 0 && (status < 100 || status > 599))
1701
+ return null;
1702
+ const since = url.searchParams.get("since") ?? void 0;
1703
+ if (since && !Number.isFinite(new Date(since).getTime())) return null;
1704
+ return {
1705
+ ...url.searchParams.get("method") ? { method: url.searchParams.get("method") ?? void 0 } : {},
1706
+ ...url.searchParams.get("path") ? { path: url.searchParams.get("path") ?? void 0 } : {},
1707
+ ...status !== void 0 ? { status } : {},
1708
+ ...url.searchParams.get("operationId") ? { operationId: url.searchParams.get("operationId") ?? void 0 } : {},
1709
+ ...since ? { since } : {},
1710
+ ...limit !== void 0 ? { limit } : {}
1711
+ };
1712
+ }
1713
+ function specSummary(context) {
1714
+ const resolvedSpec = { ...context.spec, document: context.spec.dereferenced };
1715
+ const operations = summarizeSpec(resolvedSpec);
1716
+ const schemas = Object.keys(context.spec.document.components?.schemas ?? {});
1717
+ const info = context.spec.document.info ?? {};
1718
+ return {
1719
+ openapi: context.spec.document.openapi,
1720
+ title: typeof info.title === "string" ? info.title : void 0,
1721
+ version: typeof info.version === "string" ? info.version : void 0,
1722
+ operations,
1723
+ schemas,
1724
+ warnings: context.spec.warnings
1725
+ };
1726
+ }
1727
+ function outputRequest(record) {
1728
+ return record;
1729
+ }
1730
+ async function handleInspector(method, url, context) {
1731
+ if (url.pathname !== "/__mockline" && !url.pathname.startsWith("/__mockline/")) return void 0;
1732
+ if (!context.enabled)
1733
+ return problemResult(404, "Not Found", "The inspector API is disabled for this bind address.");
1734
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
1735
+ if (pathname === "/__mockline/health" && method === "GET") {
1736
+ return json(200, {
1737
+ status: "ok",
1738
+ uptimeSeconds: Number(process.uptime().toFixed(3)),
1739
+ openapi: context.spec.document.openapi,
1740
+ operationCount: summarizeSpec(context.spec).length,
1741
+ startedAt: context.startedAt.toISOString()
1742
+ });
1743
+ }
1744
+ if (pathname === "/__mockline/spec" && method === "GET") return json(200, specSummary(context));
1745
+ if (pathname === "/__mockline/config" && method === "GET")
1746
+ return json(200, redactConfig(context.config));
1747
+ if (pathname === "/__mockline/requests" && method === "GET") {
1748
+ const filter = requestFilter(url);
1749
+ if (filter === null)
1750
+ return problemResult(
1751
+ 400,
1752
+ "Bad Request",
1753
+ "Invalid request filter. limit must be a non-negative integer, status must be an HTTP status code, and since must be a date."
1754
+ );
1755
+ return json(200, context.recorder.list(filter ?? {}));
1756
+ }
1757
+ if (pathname === "/__mockline/requests" && method === "DELETE") {
1758
+ context.recorder.clear();
1759
+ return json(200, { deleted: true });
1760
+ }
1761
+ const requestMatch = /^\/__mockline\/requests\/([^/]+)$/.exec(pathname);
1762
+ if (requestMatch && method === "GET") {
1763
+ let id = requestMatch[1] ?? "";
1764
+ try {
1765
+ id = decodeURIComponent(id);
1766
+ } catch {
1767
+ return problemResult(400, "Bad Request", "Request ID contains invalid percent encoding.");
1768
+ }
1769
+ const request = outputRequest(context.recorder.get(id));
1770
+ return request ? json(200, request) : problemResult(404, "Not Found", `No recorded request has ID ${id}.`);
1771
+ }
1772
+ if (requestMatch)
1773
+ return {
1774
+ ...problemResult(
1775
+ 405,
1776
+ "Method Not Allowed",
1777
+ `Method ${method} is not allowed for ${pathname}.`
1778
+ ),
1779
+ headers: { "content-type": "application/problem+json", allow: "GET" }
1780
+ };
1781
+ if (pathname === "/__mockline/reset" && method === "POST") {
1782
+ await context.reset();
1783
+ return json(200, { reset: true });
1784
+ }
1785
+ const allowByPath = {
1786
+ "/__mockline/health": ["GET"],
1787
+ "/__mockline/spec": ["GET"],
1788
+ "/__mockline/config": ["GET"],
1789
+ "/__mockline/requests": ["GET", "DELETE"],
1790
+ "/__mockline/reset": ["POST"]
1791
+ };
1792
+ const allowed = allowByPath[pathname];
1793
+ if (allowed)
1794
+ return {
1795
+ ...problemResult(
1796
+ 405,
1797
+ "Method Not Allowed",
1798
+ `Method ${method} is not allowed for ${pathname}.`
1799
+ ),
1800
+ headers: { "content-type": "application/problem+json", allow: allowed.join(", ") }
1801
+ };
1802
+ return problemResult(404, "Not Found", `No inspector endpoint matched ${pathname}.`);
1803
+ }
1804
+
1805
+ // src/state/collections.ts
1806
+ function isRecord5(value) {
1807
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1808
+ }
1809
+ function responseSchema(operation) {
1810
+ const responses = operation.responses;
1811
+ if (!responses) return void 0;
1812
+ const status = defaultStatus(responses);
1813
+ const response = responses[status] ?? responses.default;
1814
+ const content = response?.content;
1815
+ if (!content) return void 0;
1816
+ const media = content["application/json"] ?? Object.entries(content).find(([type]) => type.endsWith("+json"))?.[1];
1817
+ return media?.schema;
1818
+ }
1819
+ function resolveSchema2(schema, root) {
1820
+ if (!schema || typeof schema.$ref !== "string" || !schema.$ref.startsWith("#/")) return schema;
1821
+ const resolved = schema.$ref.slice(2).split("/").reduce((current, part) => {
1822
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
1823
+ return isRecord5(current) ? current[key] : void 0;
1824
+ }, root);
1825
+ return isRecord5(resolved) ? resolved : schema;
1826
+ }
1827
+ function schemaProperties(schema, root, seen = /* @__PURE__ */ new Set()) {
1828
+ if (!schema || seen.has(schema)) return {};
1829
+ seen.add(schema);
1830
+ const resolved = resolveSchema2(schema, root) ?? schema;
1831
+ const output = isRecord5(resolved.properties) ? { ...resolved.properties } : {};
1832
+ if (Array.isArray(resolved.allOf)) {
1833
+ for (const branch of resolved.allOf) {
1834
+ if (isRecord5(branch))
1835
+ Object.assign(output, schemaProperties(branch, root, seen));
1836
+ }
1837
+ }
1838
+ return output;
1839
+ }
1840
+ function schemaFingerprint(schema, root) {
1841
+ if (!schema) return void 0;
1842
+ if (schema.$ref) return `ref:${schema.$ref}`;
1843
+ return JSON.stringify(resolveSchema2(schema, root));
1844
+ }
1845
+ function listItemSchema(schema) {
1846
+ if (!schema) return void 0;
1847
+ if (schema.type === "array" && isRecord5(schema.items)) return schema.items;
1848
+ if (Array.isArray(schema.type) && schema.type.includes("array") && isRecord5(schema.items))
1849
+ return schema.items;
1850
+ return void 0;
1851
+ }
1852
+ function requestBodySchema(operation) {
1853
+ const body = isRecord5(operation.requestBody) ? operation.requestBody : void 0;
1854
+ const content = isRecord5(body?.content) ? body.content : {};
1855
+ const media = isRecord5(content["application/json"]) ? content["application/json"] : Object.entries(content).find(([type]) => type.endsWith("+json"))?.[1];
1856
+ return isRecord5(media) && isRecord5(media.schema) ? media.schema : void 0;
1857
+ }
1858
+ function pathId(path2, collectionPath) {
1859
+ if (!path2.startsWith(`${collectionPath}/`)) return void 0;
1860
+ const tail = path2.slice(collectionPath.length + 1);
1861
+ const match = /^\{([^{}]+)\}$/.exec(tail);
1862
+ return match?.[1];
1863
+ }
1864
+ function declaredQueryName(route, name) {
1865
+ return route.parameters.some((parameter) => parameter.in === "query" && parameter.name === name) ? name : void 0;
1866
+ }
1867
+ function detectCollections(router, root, idProperty = "id") {
1868
+ const collections = [];
1869
+ const listRoutes = router.routes.filter(
1870
+ (route) => route.method === "GET" && !route.path.includes("{")
1871
+ );
1872
+ for (const listRoute of listRoutes) {
1873
+ const createRoute = router.routes.find(
1874
+ (route) => route.method === "POST" && route.path === listRoute.path
1875
+ );
1876
+ if (!createRoute) continue;
1877
+ const listItem = listItemSchema(responseSchema(listRoute.operation));
1878
+ if (!listItem) continue;
1879
+ const itemCandidates = router.routes.filter(
1880
+ (route) => pathId(route.path, listRoute.path) !== void 0
1881
+ );
1882
+ const getItem = itemCandidates.find((route) => route.method === "GET");
1883
+ if (!getItem) continue;
1884
+ const getSchema = responseSchema(getItem.operation);
1885
+ if (!getSchema || schemaFingerprint(listItem, root) !== schemaFingerprint(getSchema, root))
1886
+ continue;
1887
+ const properties = schemaProperties(listItem, root);
1888
+ const idSchema = isRecord5(properties[idProperty]) ? properties[idProperty] : void 0;
1889
+ if (!idSchema) continue;
1890
+ const itemRoutes = /* @__PURE__ */ new Map();
1891
+ for (const route of itemCandidates) {
1892
+ const routeIdParam = pathId(route.path, listRoute.path);
1893
+ if (routeIdParam && routeIdParam === pathId(getItem.path, listRoute.path))
1894
+ itemRoutes.set(route.method, route);
1895
+ }
1896
+ collections.push({
1897
+ path: listRoute.path,
1898
+ idParam: pathId(getItem.path, listRoute.path) ?? idProperty,
1899
+ idProperty,
1900
+ listRoute,
1901
+ createRoute,
1902
+ itemRoutes,
1903
+ itemSchema: listItem,
1904
+ ...requestBodySchema(createRoute.operation) ? { createSchema: requestBodySchema(createRoute.operation) } : {},
1905
+ ...declaredQueryName(listRoute, "limit") ? { limitParam: "limit" } : {},
1906
+ ...declaredQueryName(listRoute, "offset") ? { offsetParam: "offset" } : {}
1907
+ });
1908
+ }
1909
+ return collections;
1910
+ }
1911
+
1912
+ // src/state/store.ts
1913
+ function isRecord6(value) {
1914
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1915
+ }
1916
+ function resolveSchema3(schema, root) {
1917
+ if (!schema || typeof schema.$ref !== "string" || !schema.$ref.startsWith("#/")) return schema;
1918
+ const resolved = schema.$ref.slice(2).split("/").reduce((current, part) => {
1919
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
1920
+ return isRecord6(current) ? current[key] : void 0;
1921
+ }, root);
1922
+ return isRecord6(resolved) ? resolved : schema;
1923
+ }
1924
+ function mergedProperties(schema, root, seen = /* @__PURE__ */ new Set()) {
1925
+ if (seen.has(schema)) return {};
1926
+ seen.add(schema);
1927
+ const resolved = resolveSchema3(schema, root) ?? schema;
1928
+ const properties = isRecord6(resolved.properties) ? resolved.properties : {};
1929
+ const merged = { ...properties };
1930
+ if (Array.isArray(resolved.allOf)) {
1931
+ for (const branch of resolved.allOf) {
1932
+ if (isRecord6(branch))
1933
+ Object.assign(merged, mergedProperties(branch, root, seen));
1934
+ }
1935
+ }
1936
+ return merged;
1937
+ }
1938
+ function defaultSuccessStatus(route) {
1939
+ const value = defaultStatus(route.operation.responses);
1940
+ const status = Number(value);
1941
+ return Number.isInteger(status) ? status : 200;
1942
+ }
1943
+ function asResponse(status, body, contentType = "application/json; charset=utf-8") {
1944
+ return { status, body, headers: { "content-type": contentType }, source: "state" };
1945
+ }
1946
+ function parseId(value) {
1947
+ return String(value);
1948
+ }
1949
+ function cleanResponseFields(item, definition, root) {
1950
+ const properties = mergedProperties(definition.itemSchema, root);
1951
+ const output = { ...item };
1952
+ for (const [name, schema] of Object.entries(properties)) {
1953
+ if (schema.writeOnly === true) delete output[name];
1954
+ }
1955
+ return output;
1956
+ }
1957
+ var ResourceStore = class {
1958
+ collections;
1959
+ root;
1960
+ seed;
1961
+ config;
1962
+ data = /* @__PURE__ */ new Map();
1963
+ routeIndex = /* @__PURE__ */ new Map();
1964
+ constructor(router, root, config) {
1965
+ this.root = root;
1966
+ this.config = config;
1967
+ this.seed = config.seed ?? 0;
1968
+ this.collections = detectCollections(router, root, config.stateful.idProperty);
1969
+ for (const definition of this.collections) {
1970
+ const state = { definition, items: [], nextNumericId: 1 };
1971
+ this.data.set(definition.path, state);
1972
+ this.routeIndex.set(`${definition.listRoute.method} ${definition.listRoute.path}`, {
1973
+ state,
1974
+ kind: "list",
1975
+ route: definition.listRoute
1976
+ });
1977
+ this.routeIndex.set(`${definition.createRoute.method} ${definition.createRoute.path}`, {
1978
+ state,
1979
+ kind: "create",
1980
+ route: definition.createRoute
1981
+ });
1982
+ for (const [method, route] of definition.itemRoutes) {
1983
+ this.routeIndex.set(`${method} ${route.path}`, { state, kind: "item", route });
1984
+ }
1985
+ }
1986
+ this.reset();
1987
+ }
1988
+ setConfig(config) {
1989
+ this.config = config;
1990
+ this.seed = config.seed ?? 0;
1991
+ }
1992
+ reset() {
1993
+ for (const state of this.data.values()) {
1994
+ state.items = [];
1995
+ if (!this.config.stateful.enabled) continue;
1996
+ state.nextNumericId = 1;
1997
+ for (let index = 0; index < this.config.stateful.seedItems; index += 1) {
1998
+ const seed = deriveSeed(this.seed, state.definition.path, index, "initial");
1999
+ const generated = generateValue(state.definition.itemSchema, {
2000
+ root: this.root,
2001
+ seed,
2002
+ hints: this.config.generation.hints,
2003
+ maxDepth: this.config.generation.maxDepth,
2004
+ minArrayItems: this.config.generation.minArrayItems,
2005
+ maxArrayItems: this.config.generation.maxArrayItems,
2006
+ mode: "response"
2007
+ });
2008
+ const item = isRecord6(generated) ? { ...generated } : {};
2009
+ const assignedId = this.ensureId(item, state, seed, true);
2010
+ item[state.definition.idProperty] = assignedId;
2011
+ state.items.push(cleanResponseFields(item, state.definition, this.root));
2012
+ }
2013
+ const numericIds = state.items.map((item) => item[state.definition.idProperty]).filter((value) => typeof value === "number" && Number.isFinite(value));
2014
+ if (numericIds.length > 0) state.nextNumericId = Math.max(...numericIds) + 1;
2015
+ }
2016
+ }
2017
+ handle(route, params, url, body) {
2018
+ const match = this.routeIndex.get(`${route.method} ${route.path}`);
2019
+ if (!match) return void 0;
2020
+ const { state, kind } = match;
2021
+ const definition = state.definition;
2022
+ if (kind === "list") {
2023
+ let items = state.items.map((item) => ({ ...item }));
2024
+ const offset = definition.offsetParam ? Math.max(0, Number(url.searchParams.get(definition.offsetParam) ?? 0) || 0) : 0;
2025
+ const limitRaw = definition.limitParam ? url.searchParams.get(definition.limitParam) : null;
2026
+ const limit = limitRaw === null ? void 0 : Math.max(0, Number(limitRaw) || 0);
2027
+ items = items.slice(offset, limit === void 0 ? void 0 : offset + limit);
2028
+ return asResponse(defaultSuccessStatus(route), items);
2029
+ }
2030
+ if (kind === "create") {
2031
+ let item;
2032
+ if (isRecord6(body)) item = { ...body };
2033
+ else {
2034
+ const schema = definition.createSchema ?? definition.itemSchema;
2035
+ const generated = generateValue(schema, {
2036
+ root: this.root,
2037
+ seed: deriveSeed(this.seed, route.method, route.path, state.items.length, "post"),
2038
+ hints: this.config.generation.hints,
2039
+ maxDepth: this.config.generation.maxDepth,
2040
+ minArrayItems: this.config.generation.minArrayItems,
2041
+ maxArrayItems: this.config.generation.maxArrayItems,
2042
+ mode: "request"
2043
+ });
2044
+ item = isRecord6(generated) ? { ...generated } : {};
2045
+ }
2046
+ const id = this.ensureId(
2047
+ item,
2048
+ state,
2049
+ deriveSeed(this.seed, route.path, state.items.length, "create"),
2050
+ false
2051
+ );
2052
+ item[definition.idProperty] = id;
2053
+ const saved = cleanResponseFields(item, definition, this.root);
2054
+ state.items.push(saved);
2055
+ return asResponse(defaultSuccessStatus(route), { ...saved });
2056
+ }
2057
+ const rawId = params[definition.idParam];
2058
+ const index = state.items.findIndex(
2059
+ (item) => parseId(item[definition.idProperty]) === String(rawId)
2060
+ );
2061
+ if (route.method === "GET") {
2062
+ if (index < 0)
2063
+ return {
2064
+ status: 404,
2065
+ body: problem(
2066
+ 404,
2067
+ "Not Found",
2068
+ `No resource with ${definition.idProperty} ${rawId} exists.`
2069
+ ),
2070
+ headers: { "content-type": "application/problem+json" },
2071
+ source: "state"
2072
+ };
2073
+ return asResponse(defaultSuccessStatus(route), { ...state.items[index] });
2074
+ }
2075
+ if (route.method === "DELETE") {
2076
+ if (index < 0) return { status: 204, source: "state" };
2077
+ state.items.splice(index, 1);
2078
+ return { status: 204, source: "state" };
2079
+ }
2080
+ if (route.method === "PUT" || route.method === "PATCH") {
2081
+ if (index < 0)
2082
+ return {
2083
+ status: 404,
2084
+ body: problem(
2085
+ 404,
2086
+ "Not Found",
2087
+ `No resource with ${definition.idProperty} ${rawId} exists.`
2088
+ ),
2089
+ headers: { "content-type": "application/problem+json" },
2090
+ source: "state"
2091
+ };
2092
+ const current = state.items[index] ?? {};
2093
+ const replacement = isRecord6(body) ? { ...body } : {};
2094
+ const item = route.method === "PATCH" ? { ...current, ...replacement } : replacement;
2095
+ item[definition.idProperty] = current[definition.idProperty] ?? rawId;
2096
+ state.items[index] = cleanResponseFields(item, definition, this.root);
2097
+ return asResponse(defaultSuccessStatus(route), { ...state.items[index] });
2098
+ }
2099
+ return void 0;
2100
+ }
2101
+ ensureId(item, state, seed, seedItem) {
2102
+ const definition = state.definition;
2103
+ const properties = mergedProperties(definition.itemSchema, this.root);
2104
+ const schema = properties[definition.idProperty] ?? { type: "string" };
2105
+ const current = item[definition.idProperty];
2106
+ const duplicate = current !== void 0 && state.items.some((existing) => parseId(existing[definition.idProperty]) === parseId(current));
2107
+ const valid = current !== void 0 && validateSchema(this.root, schema, current).valid;
2108
+ if (valid && !duplicate) return current;
2109
+ for (let attempt = 0; attempt < 50; attempt += 1) {
2110
+ let candidate;
2111
+ if (schema.type === "integer" || schema.type === "number") {
2112
+ candidate = state.nextNumericId++;
2113
+ } else {
2114
+ candidate = generateValue(schema, {
2115
+ root: this.root,
2116
+ seed: deriveSeed(seed, "id", attempt),
2117
+ hints: false,
2118
+ maxDepth: 1,
2119
+ minArrayItems: 0,
2120
+ maxArrayItems: 0
2121
+ });
2122
+ if (typeof candidate === "string" && !("format" in schema) && !("pattern" in schema) && !("enum" in schema) && !("const" in schema)) {
2123
+ candidate = seedItem ? candidate : `${candidate}-${state.items.length + 1}`;
2124
+ }
2125
+ }
2126
+ const alreadyUsed = state.items.some(
2127
+ (existing) => parseId(existing[definition.idProperty]) === parseId(candidate)
2128
+ );
2129
+ if (!alreadyUsed && validateSchema(this.root, schema, candidate).valid) return candidate;
2130
+ }
2131
+ return schema.type === "integer" || schema.type === "number" ? state.nextNumericId++ : `${definition.path.replace(/\W/g, "")}-${state.items.length + 1}`;
2132
+ }
2133
+ };
2134
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
2135
+ "connection",
2136
+ "keep-alive",
2137
+ "proxy-authenticate",
2138
+ "proxy-authorization",
2139
+ "te",
2140
+ "trailer",
2141
+ "transfer-encoding",
2142
+ "upgrade",
2143
+ "host",
2144
+ "content-length",
2145
+ "expect"
2146
+ ]);
2147
+ function isPrivateIpv4(address) {
2148
+ const parts = address.split(".").map(Number);
2149
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
2150
+ return true;
2151
+ const [a = 0, b = 0] = parts;
2152
+ return a === 0 || a === 10 || a === 127 || a >= 224 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127 || a === 198 && (b === 18 || b === 19) || a === 192 && b === 0 || a === 198 && b === 51 || a === 203 && b === 0;
2153
+ }
2154
+ function ipv4FromMappedIpv6(address) {
2155
+ const lowered = address.toLowerCase().split("%")[0] ?? "";
2156
+ if (!lowered.startsWith("::ffff:")) return void 0;
2157
+ const tail = lowered.slice("::ffff:".length);
2158
+ if (tail.includes(".")) return tail;
2159
+ const parts = tail.split(":");
2160
+ if (parts.length !== 2 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return void 0;
2161
+ const first = Number.parseInt(parts[0] ?? "0", 16);
2162
+ const second = Number.parseInt(parts[1] ?? "0", 16);
2163
+ return `${first >> 8}.${first & 255}.${second >> 8}.${second & 255}`;
2164
+ }
2165
+ function isPrivateAddress(address) {
2166
+ const version = isIP(address);
2167
+ if (version === 4) return isPrivateIpv4(address);
2168
+ if (version !== 6) return true;
2169
+ const normalized = address.toLowerCase().split("%")[0] ?? "";
2170
+ const mapped = ipv4FromMappedIpv6(normalized);
2171
+ if (mapped) return isPrivateIpv4(mapped);
2172
+ return normalized === "::" || normalized === "::1" || /^f[cd]/.test(normalized) || /^fe[89ab]/.test(normalized) || normalized.startsWith("ff") || normalized.startsWith("2001:db8:");
2173
+ }
2174
+ function stripHopByHop(headers) {
2175
+ const blocked = new Set(HOP_BY_HOP);
2176
+ const connection = headers.connection;
2177
+ if (connection)
2178
+ for (const header of connection.split(",")) blocked.add(header.trim().toLowerCase());
2179
+ const output = {};
2180
+ for (const [name, value] of Object.entries(headers)) {
2181
+ if (!blocked.has(name.toLowerCase())) output[name] = value;
2182
+ }
2183
+ return output;
2184
+ }
2185
+ function requestHeaders(headers) {
2186
+ const output = {};
2187
+ for (const [name, value] of Object.entries(headers)) {
2188
+ if (value !== void 0)
2189
+ output[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
2190
+ }
2191
+ return stripHopByHop(output);
2192
+ }
2193
+ function targetUrl(upstream, requestUrl) {
2194
+ const basePath = upstream.pathname.replace(/\/+$/, "");
2195
+ const requestedPath = requestUrl.pathname.startsWith("/") ? requestUrl.pathname : `/${requestUrl.pathname}`;
2196
+ const target = new URL(upstream);
2197
+ target.pathname = `${basePath}${requestedPath}` || "/";
2198
+ target.search = requestUrl.search;
2199
+ target.hash = "";
2200
+ return target;
2201
+ }
2202
+ async function resolvesPrivate(hostname, timeoutMs) {
2203
+ const unbracketedHost = hostname.replace(/^\[(.*)\]$/, "$1");
2204
+ if (isIP(unbracketedHost)) return isPrivateAddress(unbracketedHost);
2205
+ const normalizedHost = unbracketedHost.toLowerCase().replace(/\.$/, "");
2206
+ if (normalizedHost === "localhost" || [".localhost", ".local", ".internal", ".lan", ".home.arpa"].some(
2207
+ (suffix) => normalizedHost.endsWith(suffix)
2208
+ ))
2209
+ return true;
2210
+ let timer;
2211
+ try {
2212
+ const addresses = await Promise.race([
2213
+ lookup(unbracketedHost, { all: true, verbatim: true }),
2214
+ new Promise((_resolve, reject) => {
2215
+ timer = setTimeout(() => reject(new Error("DNS lookup timed out")), timeoutMs);
2216
+ })
2217
+ ]);
2218
+ return addresses.length === 0 || addresses.some((entry) => isPrivateAddress(entry.address));
2219
+ } finally {
2220
+ if (timer) clearTimeout(timer);
2221
+ }
2222
+ }
2223
+ function proxyProblem(status, title, detail) {
2224
+ const body = Buffer.from(JSON.stringify(problem(status, title, detail)));
2225
+ return {
2226
+ status,
2227
+ headers: {
2228
+ "content-type": "application/problem+json; charset=utf-8",
2229
+ "content-length": String(body.byteLength)
2230
+ },
2231
+ body,
2232
+ blocked: status === 403
2233
+ };
2234
+ }
2235
+ async function forwardRequest(input) {
2236
+ let upstream;
2237
+ try {
2238
+ upstream = new URL(input.upstream);
2239
+ } catch {
2240
+ return proxyProblem(502, "Bad Gateway", "The configured proxy URL is invalid.");
2241
+ }
2242
+ if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
2243
+ return proxyProblem(502, "Bad Gateway", "Mockline proxy URLs must use http or https.");
2244
+ }
2245
+ if (!input.config.allowPrivate) {
2246
+ try {
2247
+ if (await resolvesPrivate(upstream.hostname, input.config.timeoutMs)) {
2248
+ return proxyProblem(
2249
+ 403,
2250
+ "Forbidden",
2251
+ "Proxying to private, loopback, or link-local addresses is disabled. Use --proxy-allow-private only for a trusted local upstream."
2252
+ );
2253
+ }
2254
+ } catch (error) {
2255
+ return proxyProblem(
2256
+ 502,
2257
+ "Bad Gateway",
2258
+ `Could not safely resolve proxy host: ${error instanceof Error ? error.message : String(error)}`
2259
+ );
2260
+ }
2261
+ }
2262
+ const target = targetUrl(upstream, input.requestUrl);
2263
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
2264
+ const headers = requestHeaders(input.headers);
2265
+ try {
2266
+ const copiedBody = input.body && input.body.byteLength > 0 && input.method !== "GET" && input.method !== "HEAD" ? Uint8Array.from(input.body) : void 0;
2267
+ const response = await fetchImpl(target, {
2268
+ method: input.method,
2269
+ headers,
2270
+ ...copiedBody ? { body: copiedBody.buffer } : {},
2271
+ redirect: "manual",
2272
+ signal: AbortSignal.timeout(input.config.timeoutMs)
2273
+ });
2274
+ const body = Buffer.from(await response.arrayBuffer());
2275
+ const responseHeaders = {};
2276
+ response.headers.forEach((value, name) => {
2277
+ responseHeaders[name.toLowerCase()] = value;
2278
+ });
2279
+ const filtered = stripHopByHop(responseHeaders);
2280
+ delete filtered["content-length"];
2281
+ delete filtered["content-encoding"];
2282
+ return { status: response.status, headers: filtered, body, blocked: false };
2283
+ } catch (error) {
2284
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
2285
+ return proxyProblem(
2286
+ timedOut ? 504 : 502,
2287
+ timedOut ? "Gateway Timeout" : "Bad Gateway",
2288
+ timedOut ? "The upstream request timed out." : `The upstream request failed: ${error instanceof Error ? error.message : String(error)}`
2289
+ );
2290
+ }
2291
+ }
2292
+ function decodeExample(input) {
2293
+ const mediaType = input.headers["content-type"]?.split(";")[0]?.trim();
2294
+ const base = {
2295
+ method: input.method,
2296
+ path: input.path,
2297
+ ...input.operationId ? { operationId: input.operationId } : {},
2298
+ status: input.status,
2299
+ ...mediaType ? { mediaType } : {},
2300
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
2301
+ };
2302
+ if (mediaType === "application/json" || mediaType?.endsWith("+json")) {
2303
+ try {
2304
+ return { ...base, example: JSON.parse(input.body.toString("utf8")) };
2305
+ } catch {
2306
+ return { ...base, example: input.body.toString("utf8") };
2307
+ }
2308
+ }
2309
+ if (mediaType?.startsWith("text/")) return { ...base, example: input.body.toString("utf8") };
2310
+ return { ...base, bodyBase64: input.body.toString("base64") };
2311
+ }
2312
+ var ProxyExampleRecorder = class {
2313
+ file;
2314
+ queue = Promise.resolve();
2315
+ constructor(file) {
2316
+ this.file = file ? resolve(file) : void 0;
2317
+ }
2318
+ setFile(file) {
2319
+ this.file = file ? resolve(file) : void 0;
2320
+ }
2321
+ record(input) {
2322
+ if (!this.file) return;
2323
+ const example = decodeExample(input);
2324
+ this.queue = this.queue.then(async () => {
2325
+ await mkdir(dirname(this.file), { recursive: true });
2326
+ let examples = [];
2327
+ try {
2328
+ const existing = JSON.parse(await readFile(this.file, "utf8"));
2329
+ if (typeof existing === "object" && existing !== null && !Array.isArray(existing) && Array.isArray(existing.examples)) {
2330
+ examples = existing.examples;
2331
+ }
2332
+ } catch {
2333
+ }
2334
+ examples.push(example);
2335
+ const destination = this.file;
2336
+ const temporary = `${destination}.tmp`;
2337
+ await writeFile(
2338
+ temporary,
2339
+ `${JSON.stringify({ format: "mockline-proxy-examples-v1", examples }, null, 2)}
2340
+ `,
2341
+ "utf8"
2342
+ );
2343
+ await rename(temporary, destination);
2344
+ }).catch((error) => {
2345
+ console.error(
2346
+ `mockline: could not save proxy example: ${error instanceof Error ? error.message : String(error)}`
2347
+ );
2348
+ });
2349
+ }
2350
+ async flush() {
2351
+ await this.queue;
2352
+ }
2353
+ };
2354
+
2355
+ // src/validate.ts
2356
+ function isRecord7(value) {
2357
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2358
+ }
2359
+ function headerValue2(headers, name) {
2360
+ const value = headers[name.toLowerCase()] ?? headers[name];
2361
+ return Array.isArray(value) ? value.join(",") : value;
2362
+ }
2363
+ function cookieValues(header) {
2364
+ const result = /* @__PURE__ */ new Map();
2365
+ if (!header) return result;
2366
+ for (const entry of header.split(";")) {
2367
+ const separator = entry.indexOf("=");
2368
+ if (separator < 0) continue;
2369
+ const name = entry.slice(0, separator).trim();
2370
+ const value = entry.slice(separator + 1).trim();
2371
+ if (name) result.set(name, decodeURIComponentSafe(value));
2372
+ }
2373
+ return result;
2374
+ }
2375
+ function decodeURIComponentSafe(value) {
2376
+ try {
2377
+ return decodeURIComponent(value);
2378
+ } catch {
2379
+ return value;
2380
+ }
2381
+ }
2382
+ function schemaType2(schema) {
2383
+ if (Array.isArray(schema.type))
2384
+ return schema.type.find((item) => typeof item === "string" && item !== "null") ?? "string";
2385
+ return typeof schema.type === "string" ? schema.type : "string";
2386
+ }
2387
+ function coerceScalar(value, schema) {
2388
+ const type = schemaType2(schema);
2389
+ if (type === "integer") {
2390
+ if (!/^[+-]?\d+$/.test(value.trim())) return value;
2391
+ const parsed = Number(value);
2392
+ return Number.isSafeInteger(parsed) ? parsed : value;
2393
+ }
2394
+ if (type === "number") {
2395
+ if (value.trim() === "") return value;
2396
+ const parsed = Number(value);
2397
+ return Number.isFinite(parsed) ? parsed : value;
2398
+ }
2399
+ if (type === "boolean") {
2400
+ if (value.toLowerCase() === "true") return true;
2401
+ if (value.toLowerCase() === "false") return false;
2402
+ }
2403
+ if (type === "null" && value === "null") return null;
2404
+ return value;
2405
+ }
2406
+ function coerceTyped(value, schema) {
2407
+ const type = schemaType2(schema);
2408
+ if (type === "array" && Array.isArray(value)) {
2409
+ const items = isRecord7(schema.items) ? schema.items : {};
2410
+ return value.map((item) => coerceTyped(item, items));
2411
+ }
2412
+ if (type === "object" && isRecord7(value)) {
2413
+ const properties = isRecord7(schema.properties) ? schema.properties : {};
2414
+ return Object.fromEntries(
2415
+ Object.entries(value).map(([name, item]) => [
2416
+ name,
2417
+ properties[name] ? coerceTyped(item, properties[name]) : item
2418
+ ])
2419
+ );
2420
+ }
2421
+ return typeof value === "string" ? coerceScalar(value, schema) : value;
2422
+ }
2423
+ function coerceValue(raw, schema, parameter) {
2424
+ if (raw === void 0) return void 0;
2425
+ const type = schemaType2(schema);
2426
+ if (type === "array") {
2427
+ const items = isRecord7(schema.items) ? schema.items : {};
2428
+ const style = parameter?.style ?? (parameter?.in === "query" ? "form" : "simple");
2429
+ const explode = parameter?.explode ?? style === "form";
2430
+ const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
2431
+ const values = Array.isArray(raw) ? raw : raw.includes(separator) && !explode ? raw.split(separator) : [raw];
2432
+ return values.map((item) => coerceTyped(item, items));
2433
+ }
2434
+ if (type === "object") {
2435
+ let value;
2436
+ if (Array.isArray(raw)) return raw;
2437
+ if (raw.trim().startsWith("{")) {
2438
+ try {
2439
+ const parsed = JSON.parse(raw);
2440
+ value = isRecord7(parsed) ? parsed : {};
2441
+ } catch {
2442
+ value = {};
2443
+ }
2444
+ } else {
2445
+ value = {};
2446
+ const separator = parameter?.style === "simple" ? "," : "&";
2447
+ for (const pair of raw.split(separator)) {
2448
+ const [name, ...rest] = pair.split("=");
2449
+ if (name) value[name] = rest.join("=");
2450
+ }
2451
+ }
2452
+ return coerceTyped(value, schema);
2453
+ }
2454
+ return typeof raw === "string" ? coerceScalar(raw, schema) : raw.map((item) => coerceScalar(item, schema));
2455
+ }
2456
+ function parameterRawValue(parameter, input) {
2457
+ if (parameter.in === "path") return input.params[parameter.name];
2458
+ if (parameter.in === "query") {
2459
+ const values = input.url.searchParams.getAll(parameter.name);
2460
+ if (values.length > 1) return values;
2461
+ if (values.length === 1) return values[0];
2462
+ if (parameter.style === "deepObject") {
2463
+ const object = {};
2464
+ for (const [key, value] of input.url.searchParams.entries()) {
2465
+ const match = new RegExp(`^${escapeRegExp(parameter.name)}\\[([^\\]]+)\\]$`).exec(key);
2466
+ if (match?.[1]) object[match[1]] = value;
2467
+ }
2468
+ if (Object.keys(object).length > 0) return JSON.stringify(object);
2469
+ }
2470
+ if (schemaType2(parameter.schema ?? {}) === "object" && parameter.explode !== false) {
2471
+ const properties = isRecord7(parameter.schema?.properties) ? parameter.schema.properties : {};
2472
+ const object = {};
2473
+ for (const name of Object.keys(properties)) {
2474
+ const value = input.url.searchParams.get(name);
2475
+ if (value !== null) object[name] = value;
2476
+ }
2477
+ if (Object.keys(object).length > 0) return JSON.stringify(object);
2478
+ }
2479
+ return void 0;
2480
+ }
2481
+ if (parameter.in === "header") return headerValue2(input.headers, parameter.name);
2482
+ return cookieValues(headerValue2(input.headers, "cookie")).get(parameter.name);
2483
+ }
2484
+ function escapeRegExp(value) {
2485
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2486
+ }
2487
+ function formatAjvIssue(issue, location) {
2488
+ const pointer = issue.instancePath.split("/").filter(Boolean).map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")).join("/");
2489
+ const at = pointer ? `${location}/${pointer}` : location;
2490
+ const suffix = issue.message ? `: ${issue.message}` : "";
2491
+ return { location: at, message: `${at}${suffix}`, keyword: issue.keyword };
2492
+ }
2493
+ function stripReadOnly(value, seen = /* @__PURE__ */ new WeakMap()) {
2494
+ if (Array.isArray(value)) return value.map((item) => stripReadOnly(item, seen));
2495
+ if (!isRecord7(value)) return value;
2496
+ const known = seen.get(value);
2497
+ if (known) return known;
2498
+ const copy = {};
2499
+ seen.set(value, copy);
2500
+ for (const [key, child] of Object.entries(value)) {
2501
+ if (key === "properties" && isRecord7(child)) {
2502
+ const properties = {};
2503
+ for (const [property, propertySchema] of Object.entries(child)) {
2504
+ if (isRecord7(propertySchema) && propertySchema.readOnly === true) continue;
2505
+ properties[property] = stripReadOnly(propertySchema, seen);
2506
+ }
2507
+ copy[key] = properties;
2508
+ } else if (key === "required" && Array.isArray(child)) {
2509
+ const parentProperties = isRecord7(value.properties) ? value.properties : {};
2510
+ copy[key] = child.filter(
2511
+ (property) => typeof property !== "string" || !isRecord7(parentProperties[property]) || parentProperties[property]?.readOnly !== true
2512
+ );
2513
+ } else {
2514
+ copy[key] = stripReadOnly(child, seen);
2515
+ }
2516
+ }
2517
+ return copy;
2518
+ }
2519
+ function requestRootWithoutReadOnly(root) {
2520
+ return stripReadOnly(root);
2521
+ }
2522
+ function contentTypeMatches(declared, actual) {
2523
+ const expected = declared.split(";")[0]?.trim().toLowerCase() ?? "";
2524
+ const received = actual.split(";")[0]?.trim().toLowerCase() ?? "";
2525
+ return expected === received || expected === "application/*" && received.startsWith("application/") || expected === "application/json" && received.endsWith("+json");
2526
+ }
2527
+ function validateParameters(input) {
2528
+ const issues = [];
2529
+ for (const parameter of input.route.parameters) {
2530
+ if (!parameter.schema) continue;
2531
+ const location = `${parameter.in}.${parameter.name}`;
2532
+ const raw = parameterRawValue(parameter, input);
2533
+ if (raw === void 0) {
2534
+ if (parameter.required)
2535
+ issues.push({
2536
+ location,
2537
+ message: `${location}: required parameter is missing`,
2538
+ keyword: "required"
2539
+ });
2540
+ continue;
2541
+ }
2542
+ const value = coerceValue(raw, parameter.schema, parameter);
2543
+ const validation = validateSchema(input.root, parameter.schema, value);
2544
+ issues.push(...validation.errors.map((error) => formatAjvIssue(error, location)));
2545
+ }
2546
+ return issues;
2547
+ }
2548
+ function requestBodyContent(operation) {
2549
+ const body = isRecord7(operation.requestBody) ? operation.requestBody : void 0;
2550
+ const content = isRecord7(body?.content) ? body.content : {};
2551
+ return { content, contentTypes: Object.keys(content), required: body?.required === true };
2552
+ }
2553
+ function validateBody(input) {
2554
+ const issues = [];
2555
+ const details = requestBodyContent(input.route.operation);
2556
+ if (details.contentTypes.length === 0) return { issues, bodyInvalid: false };
2557
+ const contentType = headerValue2(input.headers, "content-type");
2558
+ if (input.bodyPresent && !contentType) {
2559
+ issues.push({
2560
+ location: "header.content-type",
2561
+ message: "header.content-type: Content-Type is required for this operation",
2562
+ keyword: "contentType"
2563
+ });
2564
+ }
2565
+ const selectedType = contentType && details.contentTypes.find((declared) => contentTypeMatches(declared, contentType));
2566
+ if (input.bodyPresent && contentType && !selectedType) {
2567
+ issues.push({
2568
+ location: "header.content-type",
2569
+ message: `header.content-type: expected ${details.contentTypes.join(" or ")}`,
2570
+ keyword: "contentType"
2571
+ });
2572
+ }
2573
+ if (!input.bodyPresent) {
2574
+ if (details.required)
2575
+ issues.push({
2576
+ location: "body",
2577
+ message: "body: required request body is missing",
2578
+ keyword: "required"
2579
+ });
2580
+ return { issues, bodyInvalid: issues.length > 0 };
2581
+ }
2582
+ const mediaObject = selectedType && isRecord7(details.content[selectedType]) ? details.content[selectedType] : void 0;
2583
+ const bodySchema = isRecord7(mediaObject?.schema) ? mediaObject.schema : void 0;
2584
+ if (!selectedType || !bodySchema) return { issues, bodyInvalid: issues.length > 0 };
2585
+ const root = requestRootWithoutReadOnly(input.root);
2586
+ const validation = validateSchema(root, stripReadOnly(bodySchema), input.body);
2587
+ issues.push(...validation.errors.map((error) => formatAjvIssue(error, "/body")));
2588
+ return { issues, bodyInvalid: !validation.valid || issues.length > 0 };
2589
+ }
2590
+ function validateRequest(input) {
2591
+ const parameterIssues = validateParameters(input);
2592
+ const bodyResult = validateBody(input);
2593
+ return {
2594
+ valid: parameterIssues.length === 0 && bodyResult.issues.length === 0,
2595
+ issues: [...parameterIssues, ...bodyResult.issues],
2596
+ bodyInvalid: bodyResult.bodyInvalid
2597
+ };
2598
+ }
2599
+ function checkSecurityPresence(root, operation, headers, url) {
2600
+ const requirements = operation.security ?? root.security ?? [];
2601
+ if (requirements.length === 0) return { authorized: true, missing: [] };
2602
+ const schemes = root.components?.securitySchemes ?? {};
2603
+ const missingAll = [];
2604
+ for (const requirement of requirements) {
2605
+ const missing = [];
2606
+ for (const name of Object.keys(requirement)) {
2607
+ const scheme = schemes[name];
2608
+ if (!isRecord7(scheme)) {
2609
+ missing.push(name);
2610
+ continue;
2611
+ }
2612
+ const type = String(scheme.type ?? "");
2613
+ if (type === "apiKey") {
2614
+ const location = String(scheme.in ?? "");
2615
+ const credentialName = String(scheme.name ?? "");
2616
+ const headerCredential = location === "header" ? headerValue2(headers, credentialName) : void 0;
2617
+ const present = location === "header" ? headerCredential !== void 0 : location === "query" ? url.searchParams.has(credentialName) : location === "cookie" ? cookieValues(headerValue2(headers, "cookie")).has(credentialName) : false;
2618
+ if (!present) missing.push(`${name} (${location} ${credentialName})`);
2619
+ } else if (type === "http" || type === "oauth2" || type === "openIdConnect" || type === "mutualTLS") {
2620
+ if (headerValue2(headers, "authorization") === void 0)
2621
+ missing.push(`${name} (Authorization)`);
2622
+ } else {
2623
+ missing.push(name);
2624
+ }
2625
+ }
2626
+ if (missing.length === 0) return { authorized: true, missing: [] };
2627
+ missingAll.push(...missing);
2628
+ }
2629
+ return { authorized: false, missing: [...new Set(missingAll)] };
2630
+ }
2631
+ function strictValidationStatus(result) {
2632
+ return result.bodyInvalid ? 422 : 400;
2633
+ }
2634
+
2635
+ // src/server.ts
2636
+ function isRecord8(value) {
2637
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2638
+ }
2639
+ var BodyTooLargeError = class extends Error {
2640
+ constructor() {
2641
+ super("Request body exceeded the configured size limit.");
2642
+ this.name = "BodyTooLargeError";
2643
+ }
2644
+ };
2645
+ var InvalidJsonError = class extends Error {
2646
+ constructor(message) {
2647
+ super(message);
2648
+ this.name = "InvalidJsonError";
2649
+ }
2650
+ };
2651
+ function asHeaders(headers) {
2652
+ return headers;
2653
+ }
2654
+ function isLoopback(host) {
2655
+ const value = host.toLowerCase().replace(/^\[|\]$/g, "");
2656
+ return value === "localhost" || value === "::1" || value.startsWith("127.") || value.startsWith("::ffff:127.") || isIP(value) === 0 && value.endsWith(".localhost");
2657
+ }
2658
+ function hostnameForUrl(host) {
2659
+ const value = host.replace(/^\[|\]$/g, "");
2660
+ return value.includes(":") ? `[${value}]` : value;
2661
+ }
2662
+ function applyCors(req, res, config) {
2663
+ if (!config.cors.enabled) return;
2664
+ const origin = req.headers.origin;
2665
+ const allowed = config.cors.origins;
2666
+ if (allowed.includes("*")) res.setHeader("Access-Control-Allow-Origin", "*");
2667
+ else if (origin && allowed.includes(origin)) {
2668
+ res.setHeader("Access-Control-Allow-Origin", origin);
2669
+ res.setHeader("Vary", "Origin");
2670
+ }
2671
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD");
2672
+ const requestHeaders2 = req.headers["access-control-request-headers"];
2673
+ res.setHeader(
2674
+ "Access-Control-Allow-Headers",
2675
+ requestHeaders2 ?? "Content-Type, Authorization, Prefer, X-Mockline-Status, X-Mockline-Delay, X-Mockline-Error"
2676
+ );
2677
+ res.setHeader("Access-Control-Max-Age", "600");
2678
+ }
2679
+ function writeJson(res, status, body, headers = {}, head = false) {
2680
+ if (res.destroyed || res.writableEnded) return;
2681
+ const safeStatus = Number.isInteger(status) && status >= 100 && status <= 599 ? status : 500;
2682
+ res.statusCode = safeStatus;
2683
+ for (const [name, value] of Object.entries(headers)) {
2684
+ try {
2685
+ res.setHeader(name, value);
2686
+ } catch {
2687
+ }
2688
+ }
2689
+ if (!res.hasHeader("content-type"))
2690
+ res.setHeader("content-type", "application/json; charset=utf-8");
2691
+ if (head || safeStatus === 204 || safeStatus === 304) {
2692
+ res.end();
2693
+ return;
2694
+ }
2695
+ const contentType = String(res.getHeader("content-type") ?? "").toLowerCase();
2696
+ let serialized;
2697
+ if (Buffer.isBuffer(body)) serialized = body;
2698
+ else if (typeof body === "string" && !contentType.includes("json")) serialized = body;
2699
+ else if (body === void 0) serialized = "";
2700
+ else {
2701
+ try {
2702
+ serialized = JSON.stringify(body);
2703
+ } catch {
2704
+ res.statusCode = 500;
2705
+ serialized = JSON.stringify(
2706
+ problem(
2707
+ 500,
2708
+ "Internal Server Error",
2709
+ "The selected response body could not be serialized."
2710
+ )
2711
+ );
2712
+ res.setHeader("content-type", "application/problem+json; charset=utf-8");
2713
+ }
2714
+ }
2715
+ res.setHeader(
2716
+ "content-length",
2717
+ Buffer.isBuffer(serialized) ? serialized.byteLength : Buffer.byteLength(serialized)
2718
+ );
2719
+ try {
2720
+ res.end(serialized);
2721
+ } catch {
2722
+ res.destroy();
2723
+ }
2724
+ }
2725
+ async function readBody(req, limit) {
2726
+ const declaredLength = req.headers["content-length"];
2727
+ if (typeof declaredLength === "string") {
2728
+ const parsedLength = Number(declaredLength);
2729
+ if (!Number.isFinite(parsedLength) || parsedLength < 0)
2730
+ throw new InvalidJsonError("Invalid Content-Length header.");
2731
+ if (parsedLength > limit) {
2732
+ req.resume();
2733
+ throw new BodyTooLargeError();
2734
+ }
2735
+ }
2736
+ const chunks = [];
2737
+ let size = 0;
2738
+ try {
2739
+ for await (const chunk of req) {
2740
+ const buffer = Buffer.isBuffer(chunk) ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk) : Buffer.from(String(chunk));
2741
+ size += buffer.byteLength;
2742
+ if (size > limit) {
2743
+ req.resume();
2744
+ throw new BodyTooLargeError();
2745
+ }
2746
+ chunks.push(buffer);
2747
+ }
2748
+ } catch (error) {
2749
+ if (error instanceof BodyTooLargeError) throw error;
2750
+ if (req.destroyed) throw error;
2751
+ throw new InvalidJsonError("Could not read request body.");
2752
+ }
2753
+ if (size === 0) return { present: false, raw: Buffer.alloc(0) };
2754
+ const raw = Buffer.concat(chunks);
2755
+ const text = raw.toString("utf8");
2756
+ const contentType = String(req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
2757
+ if (contentType === "application/json" || contentType.endsWith("+json")) {
2758
+ try {
2759
+ return { present: true, value: JSON.parse(text), raw };
2760
+ } catch (error) {
2761
+ const reason = error instanceof Error ? error.message : "invalid JSON";
2762
+ return { present: true, raw, parseError: `Malformed JSON request body: ${reason}` };
2763
+ }
2764
+ }
2765
+ return { present: true, value: text, raw };
2766
+ }
2767
+ function issueStrings(issues) {
2768
+ return issues.map((issue) => issue.message);
2769
+ }
2770
+ function problemResponse(status, title, detail, extra = {}) {
2771
+ return {
2772
+ status,
2773
+ body: problem(status, title, detail, extra),
2774
+ headers: { "content-type": "application/problem+json" },
2775
+ source: "generated"
2776
+ };
2777
+ }
2778
+ function createRequestAccessor(recorder) {
2779
+ const accessor = ((filter = {}) => recorder.list(filter));
2780
+ accessor.get = (id) => recorder.get(id);
2781
+ accessor.clear = () => recorder.clear();
2782
+ return accessor;
2783
+ }
2784
+ function routeUrl(req, host) {
2785
+ const headerHost = req.headers.host;
2786
+ const fallbackHost = hostnameForUrl(host);
2787
+ return new URL(req.url ?? "/", `http://${headerHost ?? fallbackHost}`);
2788
+ }
2789
+ function setServerRequestTimeout(server, timeout) {
2790
+ server.requestTimeout = timeout;
2791
+ server.headersTimeout = Math.min(timeout, 6e4);
2792
+ }
2793
+ async function createMockServer(options) {
2794
+ const loaded = await loadSpec(options.spec);
2795
+ let config = configSchema.parse({
2796
+ ...options.config ?? {},
2797
+ ...options.port !== void 0 ? { port: options.port } : {},
2798
+ ...options.host !== void 0 ? { host: options.host } : {},
2799
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
2800
+ });
2801
+ const logger = options.logger ?? createLogger();
2802
+ const inspectorRequested = options.inspector ?? config.inspector.enabled;
2803
+ const inspectorEnabled = inspectorRequested === void 0 ? isLoopback(config.host) : inspectorRequested;
2804
+ if (!isLoopback(config.host)) {
2805
+ if (inspectorEnabled)
2806
+ logger.warn(
2807
+ "The inspector API is enabled on a non-loopback bind; use it only on a trusted network."
2808
+ );
2809
+ else
2810
+ logger.warn(
2811
+ "The inspector API is disabled for a non-loopback bind. Use --inspector only on a trusted network."
2812
+ );
2813
+ }
2814
+ if (["0.0.0.0", "::"].includes(config.host))
2815
+ logger.warn(`Binding to ${config.host} exposes this mock server to the network.`);
2816
+ const routerState = { current: createRouter(loaded.document) };
2817
+ const specState = { current: loaded };
2818
+ const stateStoreState = {
2819
+ current: new ResourceStore(routerState.current, loaded.document, config)
2820
+ };
2821
+ const recorder = new RequestRecorder(config, options.clock);
2822
+ const proxyExamples = new ProxyExampleRecorder(config.proxy.recordFile);
2823
+ const requestCounters = /* @__PURE__ */ new Map();
2824
+ const sequenceIndexes = /* @__PURE__ */ new Map();
2825
+ const startedAt = options.clock?.() ?? /* @__PURE__ */ new Date();
2826
+ const boundPort = config.port;
2827
+ const boundHost = config.host;
2828
+ const resetRuntime = async () => {
2829
+ requestCounters.clear();
2830
+ sequenceIndexes.clear();
2831
+ stateStoreState.current.reset();
2832
+ };
2833
+ let closing = false;
2834
+ let watcher;
2835
+ let specReloadTimer;
2836
+ let configReloadTimer;
2837
+ const handler = async (req, res) => {
2838
+ const started = (options.clock ?? (() => /* @__PURE__ */ new Date()))().getTime();
2839
+ const method = (req.method ?? "GET").toUpperCase();
2840
+ let url2;
2841
+ try {
2842
+ url2 = routeUrl(req, config.host);
2843
+ } catch {
2844
+ url2 = new URL("/", "http://mockline.local");
2845
+ applyCors(req, res, config);
2846
+ const failure = problemResponse(400, "Bad Request", "The request URL is malformed.");
2847
+ writeJson(res, failure.status, failure.body, failure.headers);
2848
+ if (config.recording.enabled)
2849
+ recorder.record({
2850
+ method,
2851
+ url: req.url ?? "/",
2852
+ headers: asHeaders(req.headers),
2853
+ responseStatus: failure.status,
2854
+ responseSource: "error",
2855
+ durationMs: Math.max(0, (options.clock ?? (() => /* @__PURE__ */ new Date()))().getTime() - started),
2856
+ validationErrors: ["Malformed request URL"]
2857
+ });
2858
+ return;
2859
+ }
2860
+ applyCors(req, res, config);
2861
+ const pathname = normalizePath(url2.pathname);
2862
+ let body;
2863
+ let bodyPresent;
2864
+ let rawBody = Buffer.alloc(0);
2865
+ let route;
2866
+ let response = problemResponse(
2867
+ 500,
2868
+ "Internal Server Error",
2869
+ "Request processing did not complete."
2870
+ );
2871
+ let validationErrors = [];
2872
+ let source = "error";
2873
+ let responseHeaders = {};
2874
+ const forward = async (matchedRoute) => {
2875
+ if (!config.proxy.url)
2876
+ return problemResponse(502, "Bad Gateway", "No proxy upstream is configured.");
2877
+ const proxied = await forwardRequest({
2878
+ upstream: config.proxy.url,
2879
+ method,
2880
+ requestUrl: url2,
2881
+ headers: asHeaders(req.headers),
2882
+ body: rawBody,
2883
+ config: config.proxy,
2884
+ fetchImpl: options.fetch
2885
+ });
2886
+ if (!proxied.blocked && config.proxy.recordFile) {
2887
+ proxyExamples.record({
2888
+ method,
2889
+ path: `${url2.pathname}${url2.search}`,
2890
+ ...{},
2891
+ status: proxied.status,
2892
+ headers: proxied.headers,
2893
+ body: proxied.body
2894
+ });
2895
+ }
2896
+ return {
2897
+ status: proxied.status,
2898
+ body: proxied.body,
2899
+ headers: proxied.headers,
2900
+ source: "proxy"
2901
+ };
2902
+ };
2903
+ try {
2904
+ if (pathname === "/__mockline" || pathname.startsWith("/__mockline/")) {
2905
+ const inspectorResponse = await handleInspector(method, url2, {
2906
+ enabled: inspectorEnabled,
2907
+ recorder,
2908
+ config,
2909
+ spec: specState.current,
2910
+ startedAt,
2911
+ reset: resetRuntime
2912
+ });
2913
+ response = inspectorResponse ? {
2914
+ status: inspectorResponse.status,
2915
+ ...inspectorResponse.body !== void 0 ? { body: inspectorResponse.body } : {},
2916
+ ...inspectorResponse.headers ? { headers: inspectorResponse.headers } : {},
2917
+ source: "generated"
2918
+ } : problemResponse(404, "Not Found", "The inspector endpoint does not exist.");
2919
+ source = "inspector";
2920
+ } else if (method === "OPTIONS" && config.cors.enabled && req.headers["access-control-request-method"]) {
2921
+ response = { status: 204, source: "generated" };
2922
+ source = "generated";
2923
+ } else {
2924
+ const read = await readBody(req, config.requestBodyLimit);
2925
+ body = read.value;
2926
+ bodyPresent = read.present;
2927
+ rawBody = read.raw;
2928
+ if (config.proxy.all && config.proxy.url) {
2929
+ response = await forward();
2930
+ source = "proxy";
2931
+ } else {
2932
+ const match = routerState.current.match(method, pathname);
2933
+ if (!match) {
2934
+ if (read.parseError && !config.proxy.url) throw new InvalidJsonError(read.parseError);
2935
+ if (config.proxy.url) {
2936
+ response = await forward();
2937
+ source = "proxy";
2938
+ } else {
2939
+ const methods = routerState.current.methodsForPath(pathname);
2940
+ const nearest = routerState.current.nearestPath(pathname);
2941
+ if (methods.length > 0) {
2942
+ response = problemResponse(
2943
+ 405,
2944
+ "Method Not Allowed",
2945
+ `Method ${method} is not allowed for ${pathname}.`,
2946
+ nearest ? { nearestPath: nearest } : {}
2947
+ );
2948
+ responseHeaders = { Allow: methods.join(", ") };
2949
+ } else {
2950
+ response = problemResponse(
2951
+ 404,
2952
+ "Not Found",
2953
+ nearest ? `No route matched. Nearest path: ${nearest}.` : `No route matched ${pathname}.`,
2954
+ nearest ? { nearestPath: nearest } : {}
2955
+ );
2956
+ }
2957
+ source = "error";
2958
+ }
2959
+ } else {
2960
+ if (read.parseError) throw new InvalidJsonError(read.parseError);
2961
+ route = match.route;
2962
+ let authorized = true;
2963
+ if (config.enforceAuth) {
2964
+ const auth = checkSecurityPresence(
2965
+ specState.current.document,
2966
+ route.operation,
2967
+ asHeaders(req.headers),
2968
+ url2
2969
+ );
2970
+ authorized = auth.authorized;
2971
+ if (!authorized) {
2972
+ response = problemResponse(
2973
+ 401,
2974
+ "Unauthorized",
2975
+ "A credential declared by the OpenAPI security scheme is required.",
2976
+ { errors: auth.missing }
2977
+ );
2978
+ responseHeaders = { "www-authenticate": 'Bearer realm="Bearer"' };
2979
+ source = "error";
2980
+ }
2981
+ }
2982
+ if (authorized) {
2983
+ const validation = config.validate === "off" ? { valid: true, issues: [], bodyInvalid: false } : validateRequest({
2984
+ root: specState.current.document,
2985
+ route,
2986
+ params: match.params,
2987
+ url: url2,
2988
+ headers: asHeaders(req.headers),
2989
+ body,
2990
+ bodyPresent
2991
+ });
2992
+ const strictFailure = !validation.valid && config.validate === "strict";
2993
+ if (!validation.valid) {
2994
+ validationErrors = issueStrings(validation.issues);
2995
+ if (strictFailure) {
2996
+ const status = strictValidationStatus(validation);
2997
+ response = problemResponse(
2998
+ status,
2999
+ status === 422 ? "Unprocessable Content" : "Bad Request",
3000
+ "Request validation failed.",
3001
+ {
3002
+ errors: validation.issues.map((issue) => ({
3003
+ location: issue.location,
3004
+ message: issue.message
3005
+ }))
3006
+ }
3007
+ );
3008
+ source = "error";
3009
+ } else if (config.validate === "warn") {
3010
+ responseHeaders["X-Mockline-Validation"] = "failed";
3011
+ for (const error of validationErrors)
3012
+ logger.warn(`${route.operationId}: ${error}`);
3013
+ }
3014
+ }
3015
+ if (!strictFailure) {
3016
+ const key = `${route.method} ${route.path}`;
3017
+ const counter = requestCounters.get(key) ?? 0;
3018
+ requestCounters.set(key, counter + 1);
3019
+ const stateResponse = config.stateful.enabled ? () => stateStoreState.current.handle(match.route, match.params, url2, body) : void 0;
3020
+ const selected = selectResponse({
3021
+ root: specState.current.document,
3022
+ route,
3023
+ config,
3024
+ headers: asHeaders(req.headers),
3025
+ url: url2,
3026
+ body,
3027
+ requestCounter: counter,
3028
+ sequenceIndexes,
3029
+ stateResponse
3030
+ });
3031
+ response = selected.response;
3032
+ if (selected.sequenceKey) {
3033
+ const index = sequenceIndexes.get(selected.sequenceKey) ?? 0;
3034
+ sequenceIndexes.set(selected.sequenceKey, index + 1);
3035
+ }
3036
+ for (const warning of selected.warnings)
3037
+ logger.warn(`${route.operationId}: ${warning}`);
3038
+ source = response.source;
3039
+ if (responseHeaders["X-Mockline-Validation"])
3040
+ response = {
3041
+ ...response,
3042
+ headers: { ...response.headers, "X-Mockline-Validation": "failed" }
3043
+ };
3044
+ }
3045
+ }
3046
+ }
3047
+ }
3048
+ }
3049
+ } catch (error) {
3050
+ if (error instanceof BodyTooLargeError) {
3051
+ response = problemResponse(413, "Content Too Large", error.message);
3052
+ validationErrors = [error.message];
3053
+ } else if (error instanceof InvalidJsonError) {
3054
+ response = problemResponse(400, "Bad Request", error.message);
3055
+ validationErrors = [error.message];
3056
+ } else {
3057
+ logger.error(error instanceof Error ? error.message : String(error));
3058
+ response = problemResponse(
3059
+ 500,
3060
+ "Internal Server Error",
3061
+ "Mockline could not process this request."
3062
+ );
3063
+ }
3064
+ source = "error";
3065
+ }
3066
+ const headers = { ...response.headers, ...responseHeaders };
3067
+ response = { ...response, headers };
3068
+ try {
3069
+ if (response.delay && response.delay > 0) await setTimeout$1(response.delay);
3070
+ writeJson(res, response.status, response.body, headers, method === "HEAD");
3071
+ } catch (error) {
3072
+ if (!res.destroyed && !res.headersSent) {
3073
+ const failure = problem(
3074
+ 500,
3075
+ "Internal Server Error",
3076
+ error instanceof Error ? error.message : "Response failed."
3077
+ );
3078
+ writeJson(res, 500, failure, { "content-type": "application/problem+json" });
3079
+ source = "error";
3080
+ }
3081
+ } finally {
3082
+ const ended = (options.clock ?? (() => /* @__PURE__ */ new Date()))().getTime();
3083
+ if (config.recording.enabled) {
3084
+ recorder.record({
3085
+ method,
3086
+ url: `${url2.pathname}${url2.search}`,
3087
+ ...route ? { operationId: route.operationId, matchedPath: route.path } : {},
3088
+ headers: asHeaders(req.headers),
3089
+ ...body !== void 0 ? { body } : {},
3090
+ responseStatus: res.statusCode || response.status,
3091
+ responseSource: source,
3092
+ durationMs: Math.max(0, ended - started),
3093
+ validationErrors
3094
+ });
3095
+ }
3096
+ }
3097
+ };
3098
+ const httpServer = createServer((req, res) => {
3099
+ void handler(req, res).catch((error) => {
3100
+ if (!res.destroyed && !res.headersSent) {
3101
+ const detail = error instanceof Error ? error.message : "Unexpected request failure.";
3102
+ writeJson(res, 500, problem(500, "Internal Server Error", detail), {
3103
+ "content-type": "application/problem+json"
3104
+ });
3105
+ }
3106
+ });
3107
+ });
3108
+ setServerRequestTimeout(httpServer, config.requestTimeoutMs);
3109
+ httpServer.on("clientError", (_error, socket) => {
3110
+ if (socket.writable && !socket.destroyed)
3111
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
3112
+ else socket.destroy();
3113
+ });
3114
+ try {
3115
+ await new Promise((resolve6, reject) => {
3116
+ const onError = (error) => {
3117
+ httpServer.off("listening", onListening);
3118
+ reject(error);
3119
+ };
3120
+ const onListening = () => {
3121
+ httpServer.off("error", onError);
3122
+ resolve6();
3123
+ };
3124
+ httpServer.once("error", onError);
3125
+ httpServer.once("listening", onListening);
3126
+ httpServer.listen(config.port, config.host);
3127
+ });
3128
+ } catch (error) {
3129
+ const code = isRecord8(error) && error.code === "EADDRINUSE" ? "Port is already in use" : "Could not start server";
3130
+ throw new MocklineError(
3131
+ `${code} (${config.host}:${config.port}): ${error instanceof Error ? error.message : String(error)}`,
3132
+ 3,
3133
+ error
3134
+ );
3135
+ }
3136
+ const address = httpServer.address();
3137
+ const actualPort = address && typeof address !== "string" ? address.port : config.port;
3138
+ const url = `http://${hostnameForUrl(config.host)}:${actualPort}`;
3139
+ const requests = createRequestAccessor(recorder);
3140
+ if (options.watch) {
3141
+ const specPath = resolve(options.spec);
3142
+ const configPaths = options.configFile ? [resolve(options.configFile)] : [resolve("mockline.yaml"), resolve("mockline.yml"), resolve("mockline.json")];
3143
+ const watchedPaths = [.../* @__PURE__ */ new Set([specPath, ...configPaths])];
3144
+ const reloadSpec = async () => {
3145
+ try {
3146
+ const nextSpec = await loadSpec(options.spec);
3147
+ const nextRouter = createRouter(nextSpec.document);
3148
+ const nextStore = new ResourceStore(nextRouter, nextSpec.document, config);
3149
+ specState.current = nextSpec;
3150
+ routerState.current = nextRouter;
3151
+ stateStoreState.current = nextStore;
3152
+ requestCounters.clear();
3153
+ sequenceIndexes.clear();
3154
+ logger.info(`Reloaded OpenAPI spec ${options.spec}`);
3155
+ } catch (error) {
3156
+ logger.error(
3157
+ `Invalid OpenAPI change; keeping the last good spec: ${error instanceof Error ? error.message : String(error)}`
3158
+ );
3159
+ }
3160
+ };
3161
+ const reloadConfiguration = async () => {
3162
+ try {
3163
+ const loadedConfig = options.reloadConfig ? await options.reloadConfig() : (await loadConfig(options.configFile)).config;
3164
+ if (loadedConfig.port !== boundPort || loadedConfig.host !== boundHost) {
3165
+ logger.warn(
3166
+ "Hot reload cannot change the bound host or port; keeping the original listener address."
3167
+ );
3168
+ }
3169
+ const nextConfig = configSchema.parse({
3170
+ ...loadedConfig,
3171
+ port: boundPort,
3172
+ host: boundHost
3173
+ });
3174
+ const stateChanged = config.seed !== nextConfig.seed || JSON.stringify(config.stateful) !== JSON.stringify(nextConfig.stateful) || JSON.stringify(config.generation) !== JSON.stringify(nextConfig.generation);
3175
+ config = nextConfig;
3176
+ recorder.setConfig(config);
3177
+ proxyExamples.setFile(config.proxy.recordFile);
3178
+ stateStoreState.current.setConfig(config);
3179
+ if (stateChanged) stateStoreState.current.reset();
3180
+ setServerRequestTimeout(httpServer, config.requestTimeoutMs);
3181
+ logger.info("Reloaded Mockline configuration");
3182
+ } catch (error) {
3183
+ logger.error(
3184
+ `Invalid config change; keeping the last good config: ${error instanceof Error ? error.message : String(error)}`
3185
+ );
3186
+ }
3187
+ };
3188
+ const schedule = (kind) => {
3189
+ if (closing) return;
3190
+ const previous = kind === "spec" ? specReloadTimer : configReloadTimer;
3191
+ if (previous) clearTimeout(previous);
3192
+ const timer = setTimeout(() => {
3193
+ if (kind === "spec") void reloadSpec();
3194
+ else void reloadConfiguration();
3195
+ }, 120);
3196
+ timer.unref();
3197
+ if (kind === "spec") specReloadTimer = timer;
3198
+ else configReloadTimer = timer;
3199
+ };
3200
+ watcher = watch(watchedPaths, {
3201
+ ignoreInitial: true,
3202
+ awaitWriteFinish: { stabilityThreshold: 120, pollInterval: 30 }
3203
+ });
3204
+ watcher.on("all", (event, changedPath) => {
3205
+ if (!["add", "change", "unlink"].includes(event)) return;
3206
+ const absolutePath = resolve(changedPath);
3207
+ if (absolutePath === specPath) schedule("spec");
3208
+ if (configPaths.includes(absolutePath)) schedule("config");
3209
+ });
3210
+ watcher.on("error", (error) => {
3211
+ logger.error(`Watch error: ${error instanceof Error ? error.message : String(error)}`);
3212
+ });
3213
+ }
3214
+ return {
3215
+ url,
3216
+ requests,
3217
+ async reset() {
3218
+ await resetRuntime();
3219
+ },
3220
+ setOverride(key, override) {
3221
+ const parsed = configSchema.parse({ overrides: { [key]: override } }).overrides[key];
3222
+ if (!parsed) throw new MocklineError(`Invalid override for ${key}`, 1);
3223
+ config = { ...config, overrides: { ...config.overrides, [key]: parsed } };
3224
+ stateStoreState.current.setConfig(config);
3225
+ },
3226
+ async close() {
3227
+ if (closing) return;
3228
+ closing = true;
3229
+ if (specReloadTimer) clearTimeout(specReloadTimer);
3230
+ if (configReloadTimer) clearTimeout(configReloadTimer);
3231
+ await watcher?.close();
3232
+ await new Promise((resolve6, reject) => {
3233
+ httpServer.close((error) => error ? reject(error) : resolve6());
3234
+ });
3235
+ await recorder.flush();
3236
+ await proxyExamples.flush();
3237
+ }
3238
+ };
3239
+ }
3240
+
3241
+ // src/cli.ts
3242
+ var packageMetadata = createRequire(import.meta.url)("../package.json");
3243
+ var VERSION = packageMetadata.version;
3244
+ var SAMPLE_SPEC = `openapi: 3.0.3
3245
+ info:
3246
+ title: Mockline starter Petstore
3247
+ version: 1.0.0
3248
+ servers:
3249
+ - url: http://127.0.0.1:4010
3250
+ paths:
3251
+ /pets:
3252
+ get:
3253
+ operationId: listPets
3254
+ parameters:
3255
+ - name: limit
3256
+ in: query
3257
+ schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
3258
+ responses:
3259
+ '200':
3260
+ description: A list of pets
3261
+ content:
3262
+ application/json:
3263
+ examples:
3264
+ cats:
3265
+ value:
3266
+ - id: 1
3267
+ name: Milo
3268
+ tag: cat
3269
+ schema:
3270
+ type: array
3271
+ items: { $ref: '#/components/schemas/Pet' }
3272
+ post:
3273
+ operationId: createPet
3274
+ requestBody:
3275
+ required: true
3276
+ content:
3277
+ application/json:
3278
+ schema: { $ref: '#/components/schemas/PetInput' }
3279
+ responses:
3280
+ '201':
3281
+ description: Created pet
3282
+ content:
3283
+ application/json:
3284
+ schema: { $ref: '#/components/schemas/Pet' }
3285
+ /pets/{petId}:
3286
+ parameters:
3287
+ - name: petId
3288
+ in: path
3289
+ required: true
3290
+ schema: { type: integer }
3291
+ get:
3292
+ operationId: getPet
3293
+ responses:
3294
+ '200':
3295
+ description: A pet
3296
+ content:
3297
+ application/json:
3298
+ schema: { $ref: '#/components/schemas/Pet' }
3299
+ '404':
3300
+ description: Not found
3301
+ content:
3302
+ application/problem+json:
3303
+ example: { title: Not Found, status: 404 }
3304
+ put:
3305
+ operationId: replacePet
3306
+ requestBody:
3307
+ required: true
3308
+ content:
3309
+ application/json:
3310
+ schema: { $ref: '#/components/schemas/PetInput' }
3311
+ responses:
3312
+ '200':
3313
+ description: Updated pet
3314
+ content:
3315
+ application/json:
3316
+ schema: { $ref: '#/components/schemas/Pet' }
3317
+ patch:
3318
+ operationId: updatePet
3319
+ requestBody:
3320
+ required: true
3321
+ content:
3322
+ application/json:
3323
+ schema: { $ref: '#/components/schemas/PetInput' }
3324
+ responses:
3325
+ '200':
3326
+ description: Updated pet
3327
+ content:
3328
+ application/json:
3329
+ schema: { $ref: '#/components/schemas/Pet' }
3330
+ delete:
3331
+ operationId: deletePet
3332
+ responses:
3333
+ '204':
3334
+ description: Deleted pet
3335
+ components:
3336
+ schemas:
3337
+ PetInput:
3338
+ type: object
3339
+ required: [name]
3340
+ properties:
3341
+ name: { type: string, minLength: 2 }
3342
+ tag: { type: string }
3343
+ Pet:
3344
+ allOf:
3345
+ - $ref: '#/components/schemas/PetInput'
3346
+ - type: object
3347
+ required: [id]
3348
+ properties:
3349
+ id: { type: integer, minimum: 1 }
3350
+ `;
3351
+ var SAMPLE_CONFIG = `# Mockline configuration. CLI flags override these values.
3352
+ port: 4010
3353
+ host: 127.0.0.1
3354
+ prefer: example
3355
+ validate: warn
3356
+ latency: 0
3357
+ errors:
3358
+ rate: 0
3359
+ statuses: [500, 503]
3360
+ stateful:
3361
+ enabled: false
3362
+ seedItems: 3
3363
+ recording:
3364
+ enabled: true
3365
+ maxRequests: 500
3366
+ redactHeaders: [authorization, cookie, set-cookie, x-api-key, api-key, x-auth-token]
3367
+ generation:
3368
+ hints: true
3369
+ counter: false
3370
+ maxDepth: 3
3371
+ minArrayItems: 1
3372
+ maxArrayItems: 3
3373
+ controlHeaders:
3374
+ allow: true
3375
+ `;
3376
+ function optionString(options, key) {
3377
+ const value = options[key];
3378
+ return typeof value === "string" ? value : void 0;
3379
+ }
3380
+ function parseInteger2(value, name) {
3381
+ if (value === void 0) return void 0;
3382
+ const parsed = typeof value === "number" ? value : Number(value);
3383
+ if (!Number.isInteger(parsed)) throw new MocklineError(`--${name} must be an integer.`, 2);
3384
+ return parsed;
3385
+ }
3386
+ function parseDelay(value) {
3387
+ if (value === void 0) return void 0;
3388
+ const range = /^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/.exec(value);
3389
+ if (range) {
3390
+ const min = Number(range[1]);
3391
+ const max = Number(range[2]);
3392
+ if (max < min)
3393
+ throw new MocklineError("--delay range must be min-max with min no greater than max.", 2);
3394
+ return [min, max];
3395
+ }
3396
+ const fixed = Number(value);
3397
+ if (!Number.isFinite(fixed) || fixed < 0)
3398
+ throw new MocklineError("--delay must be a non-negative number or min-max range.", 2);
3399
+ return fixed;
3400
+ }
3401
+ function parseStatuses(value) {
3402
+ if (value === void 0) return void 0;
3403
+ const statuses = value.split(",").map((item) => Number(item.trim()));
3404
+ if (statuses.length === 0 || statuses.some((status) => !Number.isInteger(status) || status < 400 || status > 599)) {
3405
+ throw new MocklineError(
3406
+ "--error-statuses must be a comma-separated list of HTTP error status codes.",
3407
+ 2
3408
+ );
3409
+ }
3410
+ return statuses;
3411
+ }
3412
+ function makeConfig(base, options) {
3413
+ const port = parseInteger2(options.port, "port");
3414
+ const seed = parseInteger2(options.seed, "seed");
3415
+ const errorRate = options.errorRate === void 0 ? void 0 : Number(options.errorRate);
3416
+ if (errorRate !== void 0 && (!Number.isFinite(errorRate) || errorRate < 0 || errorRate > 1)) {
3417
+ throw new MocklineError("--error-rate must be a number from 0 to 1.", 2);
3418
+ }
3419
+ const prefer = optionString(options, "prefer");
3420
+ if (prefer && prefer !== "example" && prefer !== "generate")
3421
+ throw new MocklineError("--prefer must be example or generate.", 2);
3422
+ const validate = optionString(options, "validate");
3423
+ if (validate && !["off", "warn", "strict"].includes(validate))
3424
+ throw new MocklineError("--validate must be off, warn, or strict.", 2);
3425
+ const statuses = parseStatuses(options.errorStatuses);
3426
+ const delay = parseDelay(options.delay);
3427
+ try {
3428
+ return configSchema.parse({
3429
+ ...base,
3430
+ ...port !== void 0 ? { port } : {},
3431
+ ...options.host !== void 0 ? { host: options.host } : {},
3432
+ ...seed !== void 0 ? { seed } : {},
3433
+ ...prefer ? { prefer } : {},
3434
+ ...delay !== void 0 ? { latency: delay } : {},
3435
+ ...validate ? { validate } : {},
3436
+ errors: {
3437
+ ...base.errors,
3438
+ ...errorRate !== void 0 ? { rate: errorRate } : {},
3439
+ ...statuses ? { statuses } : {}
3440
+ },
3441
+ stateful: {
3442
+ ...base.stateful,
3443
+ ...options.stateful ? { enabled: true } : {}
3444
+ },
3445
+ proxy: {
3446
+ ...base.proxy,
3447
+ ...options.proxy ? { url: options.proxy } : {},
3448
+ ...options.proxyAll ? { all: true } : {},
3449
+ ...options.proxyAllowPrivate ? { allowPrivate: true } : {},
3450
+ ...options.proxyRecord ? { recordFile: options.proxyRecord } : {}
3451
+ },
3452
+ cors: {
3453
+ ...base.cors,
3454
+ ...options.cors !== void 0 ? { enabled: options.cors } : {}
3455
+ },
3456
+ recording: {
3457
+ ...base.recording,
3458
+ ...options.recordFile ? { file: options.recordFile } : {}
3459
+ },
3460
+ inspector: {
3461
+ ...base.inspector,
3462
+ ...options.inspector ? { enabled: true } : {}
3463
+ },
3464
+ enforceAuth: Boolean(options.enforceAuth) || base.enforceAuth,
3465
+ generation: {
3466
+ ...base.generation,
3467
+ ...options.seedCounter ? { counter: true } : {}
3468
+ }
3469
+ });
3470
+ } catch (error) {
3471
+ if (error instanceof ZodError) {
3472
+ throw new MocklineError(`Invalid CLI configuration:
3473
+ ${formatConfigErrors(error)}`, 2);
3474
+ }
3475
+ throw error;
3476
+ }
3477
+ }
3478
+ function table(spec, configKeys = []) {
3479
+ const rows = summarizeSpec(spec, configKeys);
3480
+ if (rows.length === 0) return "No operations found.";
3481
+ const widths = [
3482
+ Math.max(6, ...rows.map((row) => row.method.length)),
3483
+ Math.max(4, ...rows.map((row) => row.path.length)),
3484
+ Math.max(11, ...rows.map((row) => row.operationId.length)),
3485
+ Math.max(6, ...rows.map((row) => row.defaultStatus.length)),
3486
+ Math.max(6, ...rows.map((row) => row.source.length))
3487
+ ];
3488
+ const headers = ["METHOD", "PATH", "OPERATION ID", "STATUS", "SOURCE"];
3489
+ const line = (values) => values.map((value, index) => value.padEnd(widths[index] ?? value.length)).join(" ");
3490
+ return [
3491
+ line(headers),
3492
+ line(widths.map((width) => "-".repeat(width))),
3493
+ ...rows.map(
3494
+ (row) => line([row.method, row.path, row.operationId, row.defaultStatus, row.source])
3495
+ )
3496
+ ].join("\n");
3497
+ }
3498
+ async function initProject(options, logger) {
3499
+ const configFile = resolve("mockline.yaml");
3500
+ const specFile = resolve("openapi.yaml");
3501
+ if (!options.force && (existsSync(configFile) || existsSync(specFile))) {
3502
+ throw new MocklineError(
3503
+ "mockline.yaml or openapi.yaml already exists. Use --force to overwrite them.",
3504
+ 1
3505
+ );
3506
+ }
3507
+ await writeFile(configFile, SAMPLE_CONFIG, "utf8");
3508
+ await writeFile(specFile, SAMPLE_SPEC, "utf8");
3509
+ logger.info("Created mockline.yaml and openapi.yaml. Start with: mockline serve openapi.yaml");
3510
+ }
3511
+ async function startServing(specPath, options) {
3512
+ const loadedConfig = await loadConfig(options.config);
3513
+ const config = makeConfig(loadedConfig.config, options);
3514
+ const logger = createLogger({
3515
+ quiet: options.quiet,
3516
+ verbose: options.verbose,
3517
+ color: options.color
3518
+ });
3519
+ const server = await createMockServer({
3520
+ spec: specPath,
3521
+ config,
3522
+ inspector: options.inspector,
3523
+ logger,
3524
+ watch: options.watch === true,
3525
+ configFile: loadedConfig.path ?? options.config,
3526
+ reloadConfig: async () => {
3527
+ const freshConfig = await loadConfig(options.config);
3528
+ return makeConfig(freshConfig.config, options);
3529
+ }
3530
+ });
3531
+ logger.info(`Mock API listening at ${server.url}`);
3532
+ if (options.quiet !== true && (config.inspector.enabled ?? ["127.0.0.1", "localhost", "::1"].includes(config.host)))
3533
+ logger.info(`Inspector: ${server.url}/__mockline/health`);
3534
+ let stopping = false;
3535
+ const stop = () => {
3536
+ if (stopping) return;
3537
+ stopping = true;
3538
+ void server.close().then(() => {
3539
+ process.exitCode = 0;
3540
+ }).catch((error) => {
3541
+ logger.error(error instanceof Error ? error.message : String(error));
3542
+ process.exitCode = 3;
3543
+ });
3544
+ };
3545
+ process.once("SIGINT", stop);
3546
+ process.once("SIGTERM", stop);
3547
+ }
3548
+ async function runCli(argv = process.argv.slice(2)) {
3549
+ const cli = cac("mockline");
3550
+ cli.version(VERSION);
3551
+ cli.help();
3552
+ let action = Promise.resolve();
3553
+ let failureCode = 0;
3554
+ const logger = createLogger();
3555
+ const register = (handler) => (...args) => {
3556
+ action = handler(...args).catch((error) => {
3557
+ const code = error instanceof MocklineError ? error.exitCode : 3;
3558
+ failureCode = code;
3559
+ const message = error instanceof Error ? error.message : String(error);
3560
+ console.error(pc.red("error"), message);
3561
+ });
3562
+ };
3563
+ cli.command("init", "Create a commented config and a starter OpenAPI spec").option("--force", "Overwrite existing starter files").action(
3564
+ register(async (...args) => {
3565
+ const options = args[0] ?? {};
3566
+ await initProject(options, logger);
3567
+ })
3568
+ );
3569
+ cli.command("serve <spec>", "Start a mock API server").option("--port <port>", "Port to listen on; use 0 to select a free port").option("--host <host>", "Bind address (default 127.0.0.1)").option("--config <file>", "YAML or JSON config file").option("--seed <n>", "Deterministic generation seed").option("--prefer <mode>", "Prefer examples or schema generation").option("--delay <ms|min-max>", "Global response latency").option("--error-rate <rate>", "Probability of injected failures from 0 to 1").option("--error-statuses <codes>", "Failure statuses, comma-separated").option("--validate <mode>", "Request validation: off, warn, or strict").option("--stateful", "Enable in-memory resource collections").option("--proxy <url>", "Forward unmatched requests to an upstream").option("--proxy-all", "Forward all non-inspector requests to the upstream").option("--proxy-record <file>", "Save upstream responses as JSON examples").option("--proxy-allow-private", "Permit upstreams resolving to private addresses").option("--cors", "Enable CORS (default)").option("--no-cors", "Disable CORS").option("--inspector", "Enable the inspector on a non-loopback bind").option("--watch", "Reload valid spec and config changes").option("--record-file <path>", "Append redacted request records as JSONL").option("--enforce-auth", "Require declared security credentials to be present").option("--seed-counter", "Vary repeat requests with an operation-local counter").option("--quiet", "Suppress normal logs").option("--verbose", "Enable diagnostic logs").option("--no-color", "Disable color output").action(
3570
+ register(async (spec, optionsUnknown) => {
3571
+ const specPath = String(spec);
3572
+ const options = optionsUnknown ?? {};
3573
+ await startServing(specPath, options);
3574
+ })
3575
+ );
3576
+ cli.command("routes <spec>", "List operations and their default response source").action(
3577
+ register(async (spec) => {
3578
+ const loaded = await loadSpec(String(spec));
3579
+ const config = await loadConfig();
3580
+ console.log(table(loaded, Object.keys(config.config.overrides)));
3581
+ })
3582
+ );
3583
+ cli.command("validate <spec>", "Validate an OpenAPI document and report mocking limitations").action(
3584
+ register(async (spec) => {
3585
+ const loaded = await loadSpec(String(spec));
3586
+ const operationCount = summarizeSpec(loaded).length;
3587
+ const version = loaded.document.openapi ?? "OpenAPI";
3588
+ console.log(
3589
+ `${pc.green("Valid")} ${version}: ${operationCount} operation(s), ${loaded.warnings.length} warning(s).`
3590
+ );
3591
+ for (const warning of loaded.warnings) {
3592
+ const location = warning.operationId ? ` (${warning.operationId}${warning.path ? ` ${warning.path}` : ""})` : "";
3593
+ console.warn(`${pc.yellow("warning")}${location}: ${warning.message}`);
3594
+ }
3595
+ })
3596
+ );
3597
+ cli.on("command:*", () => {
3598
+ console.error(`Unknown command: ${argv[0] ?? ""}`);
3599
+ cli.outputHelp();
3600
+ failureCode = 2;
3601
+ });
3602
+ try {
3603
+ if (argv.length === 0) {
3604
+ cli.outputHelp();
3605
+ return 0;
3606
+ }
3607
+ cli.parse(["node", "mockline", ...argv], { run: true });
3608
+ await action;
3609
+ return failureCode;
3610
+ } catch (error) {
3611
+ const code = error instanceof MocklineError ? error.exitCode : 2;
3612
+ console.error(pc.red("error"), error instanceof Error ? error.message : String(error));
3613
+ return code;
3614
+ }
3615
+ }
3616
+ if (process.argv[1] && resolve(process.argv[1]).endsWith("/mockline")) {
3617
+ void runCli().then((code) => {
3618
+ process.exitCode = code;
3619
+ });
3620
+ } else if (process.argv[1] && process.argv[1].endsWith("cli.js")) {
3621
+ void runCli().then((code) => {
3622
+ process.exitCode = code;
3623
+ });
3624
+ }
3625
+
3626
+ export { runCli };
3627
+ //# sourceMappingURL=cli.js.map
3628
+ //# sourceMappingURL=cli.js.map