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