@apicircle/mock-server-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1078 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ buildRouter: () => buildRouter,
34
+ createMockApp: () => createMockApp,
35
+ getFreePort: () => getFreePort,
36
+ isPortFree: () => isPortFree,
37
+ openApiPathToHono: () => openApiPathToHono,
38
+ parseInsomniaToEndpoints: () => parseInsomniaToEndpoints,
39
+ parseOpenApiToEndpoints: () => parseOpenApiToEndpoints,
40
+ parsePostmanToEndpoints: () => parsePostmanToEndpoints,
41
+ parseSourceToEndpoints: () => parseSourceToEndpoints,
42
+ schemaToExample: () => schemaToExample,
43
+ startMockServer: () => startMockServer,
44
+ stopMockServer: () => stopMockServer
45
+ });
46
+ module.exports = __toCommonJS(index_exports);
47
+
48
+ // src/parsers/openapi.ts
49
+ var import_swagger_parser = __toESM(require("@apidevtools/swagger-parser"), 1);
50
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
51
+
52
+ // src/faker/schemaToExample.ts
53
+ var FORMAT_DEFAULTS = {
54
+ "date-time": "2026-04-27T00:00:00.000Z",
55
+ date: "2026-04-27",
56
+ time: "00:00:00",
57
+ email: "user@example.com",
58
+ hostname: "example.com",
59
+ ipv4: "127.0.0.1",
60
+ ipv6: "::1",
61
+ uri: "https://example.com",
62
+ url: "https://example.com",
63
+ uuid: "00000000-0000-4000-8000-000000000000",
64
+ byte: "AA==",
65
+ binary: ""
66
+ };
67
+ function schemaToExample(schema) {
68
+ if (!schema) return null;
69
+ if (schema.example !== void 0) return schema.example;
70
+ if (schema.default !== void 0) return schema.default;
71
+ if (schema.const !== void 0) return schema.const;
72
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
73
+ return schema.enum[0];
74
+ }
75
+ const branch = schema.allOf?.[0] ?? schema.oneOf?.[0] ?? schema.anyOf?.[0];
76
+ if (branch) return schemaToExample(branch);
77
+ const type = pickType(schema.type);
78
+ switch (type) {
79
+ case "string":
80
+ return schema.format ? FORMAT_DEFAULTS[schema.format] ?? "string" : "string";
81
+ case "integer":
82
+ return 0;
83
+ case "number":
84
+ return 0;
85
+ case "boolean":
86
+ return false;
87
+ case "null":
88
+ return null;
89
+ case "array": {
90
+ const items = schema.items ? schemaToExample(schema.items) : null;
91
+ return [items];
92
+ }
93
+ case "object":
94
+ default: {
95
+ const properties = schema.properties ?? {};
96
+ const required = schema.required ?? Object.keys(properties);
97
+ const out = {};
98
+ for (const key of required) {
99
+ const propSchema = properties[key];
100
+ out[key] = schemaToExample(propSchema);
101
+ }
102
+ return out;
103
+ }
104
+ }
105
+ }
106
+ function pickType(type) {
107
+ if (!type) return void 0;
108
+ if (typeof type === "string") return type;
109
+ return type.find((t) => t !== "null") ?? type[0];
110
+ }
111
+
112
+ // src/parsers/buildEndpoint.ts
113
+ function bodyTypeForContentType(contentType) {
114
+ if (!contentType) return "json";
115
+ const main = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
116
+ if (main.includes("json")) return "json";
117
+ if (main.includes("xml")) return "xml";
118
+ if (main === "application/x-www-form-urlencoded") return "urlencoded";
119
+ if (main === "multipart/form-data") return "form-data";
120
+ if (main === "application/octet-stream") return "binary";
121
+ if (main.startsWith("text/")) return "text";
122
+ return "text";
123
+ }
124
+ function bodyFromString(content, type) {
125
+ switch (type) {
126
+ case "none":
127
+ return { type: "none", content: "" };
128
+ case "binary":
129
+ return { type: "binary", content: "" };
130
+ case "form-data":
131
+ return { type: "form-data", content: "", formRows: [] };
132
+ default:
133
+ return { type, content };
134
+ }
135
+ }
136
+ function buildMockResponse(parsed) {
137
+ const contentType = parsed.headers.find((h) => h.key.toLowerCase() === "content-type")?.value;
138
+ const bodyType = bodyTypeForContentType(contentType);
139
+ return {
140
+ status: parsed.status,
141
+ headers: parsed.headers.map((h) => ({ ...h, enabled: true })),
142
+ body: bodyFromString(parsed.body, bodyType),
143
+ ...parsed.delayMs !== void 0 ? { delayMs: parsed.delayMs } : {}
144
+ };
145
+ }
146
+ function buildMockEndpoint(input) {
147
+ return {
148
+ id: input.id,
149
+ name: input.name ?? `${input.method} ${input.pathPattern}`,
150
+ method: input.method,
151
+ pathPattern: input.pathPattern,
152
+ description: input.description,
153
+ requestSchema: { pathParams: [], queryParams: [], headers: [], cookies: [] },
154
+ requestValidation: [],
155
+ responseRules: [],
156
+ defaultResponse: buildMockResponse(input.response),
157
+ example: input.example
158
+ };
159
+ }
160
+
161
+ // src/parsers/openapi.ts
162
+ var SUPPORTED_METHODS = [
163
+ "GET",
164
+ "POST",
165
+ "PUT",
166
+ "PATCH",
167
+ "DELETE",
168
+ "HEAD",
169
+ "OPTIONS"
170
+ ];
171
+ async function parseOpenApiToEndpoints(source, format = "json", opts = {}) {
172
+ const warnings = [];
173
+ const raw = format === "yaml" ? safeYamlLoad(source) : safeJsonParse(source);
174
+ if (!raw || typeof raw !== "object") {
175
+ return { endpoints: [], warnings: ["Could not parse OpenAPI source"] };
176
+ }
177
+ let api;
178
+ try {
179
+ const dereffed = await import_swagger_parser.default.dereference(raw);
180
+ api = dereffed;
181
+ } catch (err) {
182
+ warnings.push(
183
+ `swagger-parser failed: ${err instanceof Error ? err.message : "unknown error"}; falling back to raw spec`
184
+ );
185
+ api = raw;
186
+ }
187
+ const paths = api.paths ?? {};
188
+ const endpoints = [];
189
+ let endpointId = 0;
190
+ for (const [path, ops] of Object.entries(paths)) {
191
+ if (!ops || typeof ops !== "object") continue;
192
+ for (const method of Object.keys(ops)) {
193
+ const upper = method.toUpperCase();
194
+ if (!SUPPORTED_METHODS.includes(upper)) continue;
195
+ const op = ops[method];
196
+ if (!op || typeof op !== "object") continue;
197
+ const built = buildEndpointFromOp(path, upper, op, opts, warnings, endpointId++);
198
+ if (built) endpoints.push(built);
199
+ }
200
+ }
201
+ return { endpoints, warnings };
202
+ }
203
+ function buildEndpointFromOp(path, method, op, opts, warnings, index) {
204
+ const responses = op.responses ?? {};
205
+ const candidates = Object.keys(responses).filter((code) => /^2\d\d$/.test(code)).map((code) => Number(code));
206
+ if (candidates.length === 0) {
207
+ warnings.push(`No 2xx response defined for ${method} ${path} \u2014 skipping`);
208
+ return null;
209
+ }
210
+ const status = opts.preferStatus ? candidates.includes(opts.preferStatus) ? opts.preferStatus : Math.min(...candidates) : Math.min(...candidates);
211
+ const response = responses[String(status)];
212
+ if (!response) {
213
+ warnings.push(`Response ${status} missing for ${method} ${path}`);
214
+ return null;
215
+ }
216
+ const { contentType, body, exampleName } = pickResponsePayload(response, op);
217
+ const headers = pickResponseHeaders(response, contentType);
218
+ return buildMockEndpoint({
219
+ id: `op-${index}-${method.toLowerCase()}-${slug(path)}`,
220
+ method,
221
+ pathPattern: path,
222
+ example: exampleName,
223
+ response: { status, headers, body }
224
+ });
225
+ }
226
+ function pickResponsePayload(response, op) {
227
+ if (response.content) {
228
+ const mediaTypes = Object.keys(response.content);
229
+ const preferred = mediaTypes.find((m) => m.toLowerCase().includes("json")) ?? mediaTypes[0] ?? "application/json";
230
+ const entry = response.content[preferred];
231
+ if (entry) {
232
+ if (entry.example !== void 0) {
233
+ return {
234
+ contentType: preferred,
235
+ body: stringifyForContentType(entry.example, preferred),
236
+ exampleName: void 0
237
+ };
238
+ }
239
+ if (entry.examples) {
240
+ const firstExampleName = Object.keys(entry.examples)[0];
241
+ if (firstExampleName) {
242
+ const v = entry.examples[firstExampleName];
243
+ return {
244
+ contentType: preferred,
245
+ body: stringifyForContentType(v?.value, preferred),
246
+ exampleName: firstExampleName
247
+ };
248
+ }
249
+ }
250
+ if (entry.schema) {
251
+ return {
252
+ contentType: preferred,
253
+ body: stringifyForContentType(schemaToExample(entry.schema), preferred),
254
+ exampleName: void 0
255
+ };
256
+ }
257
+ }
258
+ }
259
+ if (response.schema) {
260
+ const ct = (op.produces && op.produces[0]) ?? "application/json";
261
+ return {
262
+ contentType: ct,
263
+ body: stringifyForContentType(schemaToExample(response.schema), ct),
264
+ exampleName: void 0
265
+ };
266
+ }
267
+ if (response.examples) {
268
+ const firstName = Object.keys(response.examples)[0];
269
+ if (firstName) {
270
+ const v = response.examples[firstName];
271
+ return {
272
+ contentType: firstName,
273
+ body: stringifyForContentType(v, firstName),
274
+ exampleName: firstName
275
+ };
276
+ }
277
+ }
278
+ return {
279
+ contentType: "application/json",
280
+ body: "{}",
281
+ exampleName: void 0
282
+ };
283
+ }
284
+ function pickResponseHeaders(response, contentType) {
285
+ const headers = [{ key: "Content-Type", value: contentType }];
286
+ if (response.headers) {
287
+ for (const [name, def] of Object.entries(response.headers)) {
288
+ if (name.toLowerCase() === "content-type") continue;
289
+ const value = def.example !== void 0 ? def.example : schemaToExample(def.schema);
290
+ if (value === void 0 || value === null) continue;
291
+ const text = typeof value === "string" ? value : JSON.stringify(value);
292
+ headers.push({ key: name, value: text });
293
+ }
294
+ }
295
+ return headers;
296
+ }
297
+ function stringifyForContentType(value, contentType) {
298
+ if (value === void 0 || value === null) return "";
299
+ if (typeof value === "string") return value;
300
+ if (contentType.toLowerCase().includes("json")) {
301
+ return JSON.stringify(value, null, 2);
302
+ }
303
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
304
+ return JSON.stringify(value);
305
+ }
306
+ function slug(s) {
307
+ return s.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "root";
308
+ }
309
+ function safeJsonParse(s) {
310
+ try {
311
+ return JSON.parse(s);
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+ function safeYamlLoad(s) {
317
+ try {
318
+ return import_js_yaml.default.load(s);
319
+ } catch {
320
+ return safeJsonParse(s);
321
+ }
322
+ }
323
+
324
+ // src/parsers/postman.ts
325
+ var SUPPORTED_METHODS2 = [
326
+ "GET",
327
+ "POST",
328
+ "PUT",
329
+ "PATCH",
330
+ "DELETE",
331
+ "HEAD",
332
+ "OPTIONS"
333
+ ];
334
+ function parsePostmanToEndpoints(source) {
335
+ const warnings = [];
336
+ let parsed;
337
+ try {
338
+ parsed = JSON.parse(source);
339
+ } catch {
340
+ return { endpoints: [], warnings: ["Could not parse Postman collection JSON"] };
341
+ }
342
+ const endpoints = [];
343
+ let endpointId = 0;
344
+ walkItems(parsed.item ?? [], (item) => {
345
+ if (!item.request) return;
346
+ const method = (item.request.method ?? "GET").toUpperCase();
347
+ if (!SUPPORTED_METHODS2.includes(method)) {
348
+ warnings.push(`Skipping ${method} (unsupported method): ${item.name ?? "(unnamed)"}`);
349
+ return;
350
+ }
351
+ const path = extractPath(item.request.url);
352
+ if (!path) {
353
+ warnings.push(`Skipping request with no extractable path: ${item.name ?? "(unnamed)"}`);
354
+ return;
355
+ }
356
+ const example = item.response?.[0];
357
+ if (example) {
358
+ endpoints.push(
359
+ buildMockEndpoint({
360
+ id: `pm-${endpointId++}-${slug2(path)}`,
361
+ name: item.name,
362
+ method,
363
+ pathPattern: path,
364
+ example: example.name,
365
+ response: {
366
+ // Postman's `code` is the canonical numeric status; `status` is a
367
+ // human-readable label that *sometimes* parses as a number. Try
368
+ // both, fall back to 200 only when neither yields a finite number.
369
+ status: example.code ?? (Number.isFinite(Number(example.status)) ? Number(example.status) : 200),
370
+ headers: (example.header ?? []).map((h) => ({ key: h.key, value: h.value })),
371
+ body: example.body ?? ""
372
+ }
373
+ })
374
+ );
375
+ } else {
376
+ endpoints.push(
377
+ buildMockEndpoint({
378
+ id: `pm-${endpointId++}-${slug2(path)}`,
379
+ name: item.name,
380
+ method,
381
+ pathPattern: path,
382
+ response: {
383
+ status: 200,
384
+ headers: [{ key: "Content-Type", value: "application/json" }],
385
+ body: "{}"
386
+ }
387
+ })
388
+ );
389
+ }
390
+ });
391
+ return { endpoints, warnings };
392
+ }
393
+ function walkItems(items, visit) {
394
+ for (const item of items) {
395
+ if (item.item) walkItems(item.item, visit);
396
+ else if (item.request) visit(item);
397
+ }
398
+ }
399
+ function extractPath(url) {
400
+ if (!url) return null;
401
+ if (typeof url === "string") {
402
+ return urlToPath(url);
403
+ }
404
+ if (url.raw) return urlToPath(url.raw);
405
+ if (url.path) {
406
+ const segments = Array.isArray(url.path) ? url.path : [url.path];
407
+ return "/" + segments.filter(Boolean).join("/");
408
+ }
409
+ return null;
410
+ }
411
+ function urlToPath(raw) {
412
+ try {
413
+ const parsed = new URL(raw.replace(/^https?:\/\/[^/]*$/, raw + "/"));
414
+ return parsed.pathname || "/";
415
+ } catch {
416
+ const idx = raw.indexOf("/");
417
+ return idx === -1 ? "/" : raw.slice(idx);
418
+ }
419
+ }
420
+ function slug2(s) {
421
+ return s.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "root";
422
+ }
423
+
424
+ // src/parsers/insomnia.ts
425
+ var SUPPORTED_METHODS3 = [
426
+ "GET",
427
+ "POST",
428
+ "PUT",
429
+ "PATCH",
430
+ "DELETE",
431
+ "HEAD",
432
+ "OPTIONS"
433
+ ];
434
+ function parseInsomniaToEndpoints(source) {
435
+ const warnings = [];
436
+ let parsed;
437
+ try {
438
+ parsed = JSON.parse(source);
439
+ } catch {
440
+ return { endpoints: [], warnings: ["Could not parse Insomnia export JSON"] };
441
+ }
442
+ const endpoints = [];
443
+ let endpointId = 0;
444
+ for (const r of parsed.resources ?? []) {
445
+ if (r._type !== "request") continue;
446
+ const method = (r.method ?? "GET").toUpperCase();
447
+ if (!SUPPORTED_METHODS3.includes(method)) {
448
+ warnings.push(`Skipping ${method} (unsupported method): ${r.name ?? "(unnamed)"}`);
449
+ continue;
450
+ }
451
+ const path = extractPath2(r.url);
452
+ if (!path) {
453
+ warnings.push(`Skipping request with no path: ${r.name ?? "(unnamed)"}`);
454
+ continue;
455
+ }
456
+ endpoints.push(
457
+ buildMockEndpoint({
458
+ id: `ins-${endpointId++}-${slug3(path)}`,
459
+ name: r.name,
460
+ method,
461
+ pathPattern: path,
462
+ response: {
463
+ status: 200,
464
+ headers: [{ key: "Content-Type", value: "application/json" }],
465
+ body: "{}"
466
+ }
467
+ })
468
+ );
469
+ }
470
+ return { endpoints, warnings };
471
+ }
472
+ function extractPath2(url) {
473
+ if (!url) return null;
474
+ try {
475
+ return new URL(url).pathname || "/";
476
+ } catch {
477
+ const idx = url.indexOf("/");
478
+ return idx === -1 ? "/" : url.slice(idx);
479
+ }
480
+ }
481
+ function slug3(s) {
482
+ return s.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "root";
483
+ }
484
+
485
+ // src/handlers/buildRouter.ts
486
+ var import_hono = require("hono");
487
+
488
+ // src/cors.ts
489
+ var import_cors = require("hono/cors");
490
+ function buildCors(config) {
491
+ if (!config.enabled) return null;
492
+ if (config.origins.length === 0) return null;
493
+ return (0, import_cors.cors)({
494
+ origin: config.origins,
495
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
496
+ allowHeaders: ["Content-Type", "Authorization", "Accept", "X-Requested-With"],
497
+ exposeHeaders: ["Content-Type"],
498
+ credentials: false,
499
+ maxAge: 600
500
+ });
501
+ }
502
+
503
+ // src/validation/evaluate.ts
504
+ function evaluateValidation(endpoint, ctx) {
505
+ for (const rule of endpoint.requestValidation) {
506
+ if (!rule.enabled) continue;
507
+ if (!rulePasses(rule, ctx)) {
508
+ return rule.failResponse;
509
+ }
510
+ }
511
+ return null;
512
+ }
513
+ function rulePasses(rule, ctx) {
514
+ switch (rule.kind) {
515
+ case "header-required": {
516
+ const v = ctx.headers[rule.target.toLowerCase()];
517
+ return v !== void 0 && v !== "";
518
+ }
519
+ case "header-equals": {
520
+ const v = ctx.headers[rule.target.toLowerCase()];
521
+ return v !== void 0 && v === (rule.expected ?? "");
522
+ }
523
+ case "header-matches": {
524
+ const v = ctx.headers[rule.target.toLowerCase()];
525
+ if (v === void 0) return false;
526
+ return safeRegexTest(rule.expected, v);
527
+ }
528
+ case "query-required": {
529
+ const v = ctx.query[rule.target];
530
+ return v !== void 0 && v !== "";
531
+ }
532
+ case "query-equals": {
533
+ const v = ctx.query[rule.target];
534
+ return v !== void 0 && v === (rule.expected ?? "");
535
+ }
536
+ case "query-matches": {
537
+ const v = ctx.query[rule.target];
538
+ if (v === void 0) return false;
539
+ return safeRegexTest(rule.expected, v);
540
+ }
541
+ case "cookie-required": {
542
+ const v = ctx.cookies[rule.target];
543
+ return v !== void 0 && v !== "";
544
+ }
545
+ case "body-required": {
546
+ if (ctx.bodyJson !== void 0 && ctx.bodyJson !== null) {
547
+ if (Array.isArray(ctx.bodyJson)) return ctx.bodyJson.length > 0;
548
+ if (typeof ctx.bodyJson === "object") {
549
+ return Object.keys(ctx.bodyJson).length > 0;
550
+ }
551
+ return true;
552
+ }
553
+ return ctx.bodyText.length > 0;
554
+ }
555
+ case "content-type-equals": {
556
+ const ct = ctx.headers["content-type"] ?? "";
557
+ const main = ct.split(";")[0]?.trim().toLowerCase() ?? "";
558
+ return main === (rule.expected ?? "").trim().toLowerCase();
559
+ }
560
+ }
561
+ }
562
+ function safeRegexTest(pattern, value) {
563
+ if (pattern === void 0) return false;
564
+ try {
565
+ const m = /^\/(.*)\/([a-z]*)$/.exec(pattern);
566
+ const re = m ? new RegExp(m[1], m[2]) : new RegExp(pattern);
567
+ return re.test(value);
568
+ } catch {
569
+ return false;
570
+ }
571
+ }
572
+
573
+ // src/rules/evaluate.ts
574
+ function evaluateResponseRules(endpoint, ctx) {
575
+ for (const rule of endpoint.responseRules) {
576
+ if (!rule.enabled) continue;
577
+ if (rule.when.length === 0) continue;
578
+ if (rule.when.every((clause) => matchesClause(clause, ctx))) {
579
+ return rule.response;
580
+ }
581
+ }
582
+ return endpoint.defaultResponse;
583
+ }
584
+ function matchesClause(clause, ctx) {
585
+ const actual = readScopeValue(clause.scope, clause.target, ctx);
586
+ return applyOp(clause.op, actual, clause.value);
587
+ }
588
+ function readScopeValue(scope, target, ctx) {
589
+ switch (scope) {
590
+ case "query":
591
+ return ctx.query[target];
592
+ case "pathParam":
593
+ return ctx.pathParams[target];
594
+ case "header":
595
+ return ctx.headers[target.toLowerCase()];
596
+ case "cookie":
597
+ return ctx.cookies[target];
598
+ case "body-json-path":
599
+ return resolveJsonPathString(ctx.bodyJson, target);
600
+ }
601
+ }
602
+ function applyOp(op, actual, expected) {
603
+ switch (op) {
604
+ case "present":
605
+ return actual !== void 0 && actual !== "";
606
+ case "absent":
607
+ return actual === void 0 || actual === "";
608
+ case "equals":
609
+ return actual !== void 0 && expected !== void 0 && actual === expected;
610
+ case "not-equals":
611
+ return actual === void 0 || expected === void 0 || actual !== expected;
612
+ case "matches": {
613
+ if (actual === void 0 || expected === void 0) return false;
614
+ try {
615
+ const re = compileMaybeFlaggedRegex(expected);
616
+ return re.test(actual);
617
+ } catch {
618
+ return false;
619
+ }
620
+ }
621
+ case "gt":
622
+ case "lt":
623
+ case "gte":
624
+ case "lte": {
625
+ if (actual === void 0 || expected === void 0) return false;
626
+ const a = Number(actual);
627
+ const e = Number(expected);
628
+ if (!Number.isFinite(a) || !Number.isFinite(e)) return false;
629
+ switch (op) {
630
+ case "gt":
631
+ return a > e;
632
+ case "lt":
633
+ return a < e;
634
+ case "gte":
635
+ return a >= e;
636
+ case "lte":
637
+ return a <= e;
638
+ }
639
+ }
640
+ }
641
+ }
642
+ function compileMaybeFlaggedRegex(input) {
643
+ const m = /^\/(.*)\/([a-z]*)$/.exec(input);
644
+ if (m) return new RegExp(m[1], m[2]);
645
+ return new RegExp(input);
646
+ }
647
+ function resolveJsonPathString(body, jsonPath) {
648
+ const value = resolveJsonPath(body, jsonPath);
649
+ if (value === void 0 || value === null) return void 0;
650
+ if (typeof value === "string") return value;
651
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
652
+ return JSON.stringify(value);
653
+ }
654
+ function resolveJsonPath(body, jsonPath) {
655
+ if (body === void 0 || body === null) return void 0;
656
+ let path = jsonPath.trim();
657
+ if (path.startsWith("$")) path = path.slice(1);
658
+ if (path.startsWith(".")) path = path.slice(1);
659
+ if (path === "") return body;
660
+ let cursor = body;
661
+ const re = /([^.[\]]+)|\[([^\]]+)\]/g;
662
+ let match;
663
+ while ((match = re.exec(path)) !== null) {
664
+ if (cursor === void 0 || cursor === null) return void 0;
665
+ const key = match[1] ?? match[2];
666
+ if (key === void 0) return void 0;
667
+ if (Array.isArray(cursor)) {
668
+ const idx = Number(key);
669
+ if (!Number.isInteger(idx)) return void 0;
670
+ cursor = cursor[idx];
671
+ continue;
672
+ }
673
+ if (typeof cursor === "object") {
674
+ cursor = cursor[key];
675
+ continue;
676
+ }
677
+ return void 0;
678
+ }
679
+ return cursor;
680
+ }
681
+
682
+ // src/response/applyMultipliers.ts
683
+ function applyMultipliers(response, ctx) {
684
+ const multipliers = response.multipliers;
685
+ if (!multipliers || multipliers.length === 0) return response;
686
+ if (response.body.type !== "json") return response;
687
+ let parsed;
688
+ try {
689
+ parsed = JSON.parse(response.body.content);
690
+ } catch {
691
+ return response;
692
+ }
693
+ let mutated = false;
694
+ for (const multiplier of multipliers) {
695
+ const count = resolveCount(multiplier, ctx);
696
+ if (count === null) continue;
697
+ const next = applyOneMultiplier(parsed, multiplier.targetJsonPath, count);
698
+ if (next.changed) {
699
+ parsed = next.value;
700
+ mutated = true;
701
+ }
702
+ }
703
+ if (!mutated) return response;
704
+ return {
705
+ ...response,
706
+ body: { type: "json", content: JSON.stringify(parsed) }
707
+ };
708
+ }
709
+ function resolveCount(multiplier, ctx) {
710
+ const raw = readSource(multiplier, ctx);
711
+ let count;
712
+ if (raw === void 0 || raw === "" || raw === null) {
713
+ count = multiplier.defaultCount;
714
+ } else {
715
+ const parsed = Number(raw);
716
+ if (!Number.isFinite(parsed)) {
717
+ count = multiplier.defaultCount;
718
+ } else {
719
+ count = Math.trunc(parsed);
720
+ }
721
+ }
722
+ if (multiplier.min !== void 0 && count < multiplier.min) count = multiplier.min;
723
+ if (multiplier.max !== void 0 && count > multiplier.max) count = multiplier.max;
724
+ if (count < 0) count = 0;
725
+ return count;
726
+ }
727
+ function readSource(multiplier, ctx) {
728
+ const { kind, key } = multiplier.source;
729
+ switch (kind) {
730
+ case "query":
731
+ return ctx.query[key];
732
+ case "pathParam":
733
+ return ctx.pathParams[key];
734
+ case "header":
735
+ return ctx.headers[key.toLowerCase()];
736
+ case "body-json-path":
737
+ return resolveJsonPath(ctx.bodyJson, key);
738
+ }
739
+ }
740
+ function applyOneMultiplier(body, jsonPath, count) {
741
+ const segments = parsePathSegments(jsonPath);
742
+ if (segments.length === 0) {
743
+ if (Array.isArray(body) && body.length > 0) {
744
+ return { changed: true, value: repeatArray(body, count) };
745
+ }
746
+ return { changed: false, value: body };
747
+ }
748
+ return setAtPath(body, segments, 0, count);
749
+ }
750
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
751
+ function parsePathSegments(jsonPath) {
752
+ let path = jsonPath.trim();
753
+ if (path.startsWith("$")) path = path.slice(1);
754
+ if (path.startsWith(".")) path = path.slice(1);
755
+ if (path === "") return [];
756
+ const out = [];
757
+ const re = /([^.[\]]+)|\[([^\]]+)\]/g;
758
+ let match;
759
+ while ((match = re.exec(path)) !== null) {
760
+ if (match[1] !== void 0) {
761
+ if (FORBIDDEN_KEYS.has(match[1])) return [];
762
+ out.push({ kind: "key", name: match[1] });
763
+ } else if (match[2] !== void 0) {
764
+ const n = Number(match[2]);
765
+ if (Number.isInteger(n)) {
766
+ out.push({ kind: "index", idx: n });
767
+ } else {
768
+ if (FORBIDDEN_KEYS.has(match[2])) return [];
769
+ out.push({ kind: "key", name: match[2] });
770
+ }
771
+ }
772
+ }
773
+ return out;
774
+ }
775
+ function setAtPath(cursor, segments, depth, count) {
776
+ if (depth === segments.length) {
777
+ if (Array.isArray(cursor) && cursor.length > 0) {
778
+ return { changed: true, value: repeatArray(cursor, count) };
779
+ }
780
+ return { changed: false, value: cursor };
781
+ }
782
+ const seg = segments[depth];
783
+ if (seg.kind === "index") {
784
+ if (!Array.isArray(cursor)) return { changed: false, value: cursor };
785
+ if (seg.idx < 0 || seg.idx >= cursor.length) return { changed: false, value: cursor };
786
+ const inner2 = setAtPath(cursor[seg.idx], segments, depth + 1, count);
787
+ if (!inner2.changed) return { changed: false, value: cursor };
788
+ const next = cursor.slice();
789
+ next[seg.idx] = inner2.value;
790
+ return { changed: true, value: next };
791
+ }
792
+ if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
793
+ return { changed: false, value: cursor };
794
+ }
795
+ const obj = cursor;
796
+ if (!(seg.name in obj)) return { changed: false, value: cursor };
797
+ const inner = setAtPath(obj[seg.name], segments, depth + 1, count);
798
+ if (!inner.changed) return { changed: false, value: cursor };
799
+ return { changed: true, value: { ...obj, [seg.name]: inner.value } };
800
+ }
801
+ function repeatArray(arr, count) {
802
+ if (count <= 0) return [];
803
+ const template = arr[0];
804
+ const out = [];
805
+ for (let i = 0; i < count; i++) {
806
+ out.push(cloneShallow(template));
807
+ }
808
+ return out;
809
+ }
810
+ function cloneShallow(value) {
811
+ if (value === null || typeof value !== "object") return value;
812
+ if (Array.isArray(value)) return value.slice();
813
+ return { ...value };
814
+ }
815
+
816
+ // src/handlers/buildRouter.ts
817
+ function buildRouter(server, opts = {}) {
818
+ const app = new import_hono.Hono();
819
+ const corsMiddleware = buildCors(server.cors);
820
+ if (corsMiddleware) app.use("*", corsMiddleware);
821
+ const sorted = [...server.endpoints].sort((a, b) => {
822
+ const aHasParam = /\{/.test(a.pathPattern);
823
+ const bHasParam = /\{/.test(b.pathPattern);
824
+ if (aHasParam !== bHasParam) return aHasParam ? 1 : -1;
825
+ return b.pathPattern.length - a.pathPattern.length;
826
+ });
827
+ for (const endpoint of sorted) {
828
+ const honoPath = openApiPathToHono(endpoint.pathPattern);
829
+ const handler = makeHandler(endpoint, opts);
830
+ switch (endpoint.method) {
831
+ case "GET":
832
+ app.get(honoPath, handler);
833
+ break;
834
+ case "POST":
835
+ app.post(honoPath, handler);
836
+ break;
837
+ case "PUT":
838
+ app.put(honoPath, handler);
839
+ break;
840
+ case "PATCH":
841
+ app.patch(honoPath, handler);
842
+ break;
843
+ case "DELETE":
844
+ app.delete(honoPath, handler);
845
+ break;
846
+ case "HEAD":
847
+ case "OPTIONS":
848
+ app.on(endpoint.method, honoPath, handler);
849
+ break;
850
+ }
851
+ }
852
+ app.notFound(
853
+ (c) => c.json(
854
+ {
855
+ error: "No mock endpoint matches this path",
856
+ method: c.req.method,
857
+ path: new URL(c.req.url).pathname
858
+ },
859
+ 404
860
+ )
861
+ );
862
+ return app;
863
+ }
864
+ function makeHandler(endpoint, opts) {
865
+ return async (c) => {
866
+ opts.onRequest?.({
867
+ endpointId: endpoint.id,
868
+ method: endpoint.method,
869
+ path: endpoint.pathPattern
870
+ });
871
+ const ctx = await buildRequestContext(c);
872
+ const failResponse = evaluateValidation(endpoint, ctx);
873
+ if (failResponse) {
874
+ return await respond(c, failResponse, ctx);
875
+ }
876
+ const matched = evaluateResponseRules(endpoint, ctx);
877
+ return await respond(c, matched, ctx);
878
+ };
879
+ }
880
+ async function buildRequestContext(c) {
881
+ const url = new URL(c.req.url);
882
+ const queryEntries = Array.from(url.searchParams.entries());
883
+ const query = /* @__PURE__ */ Object.create(null);
884
+ for (const [k, v] of queryEntries) {
885
+ if (!(k in query)) query[k] = v;
886
+ }
887
+ const headers = /* @__PURE__ */ Object.create(null);
888
+ for (const [k, v] of c.req.raw.headers.entries()) {
889
+ headers[k.toLowerCase()] = v;
890
+ }
891
+ const honoParams = c.req.param();
892
+ const pathParams = /* @__PURE__ */ Object.create(null);
893
+ for (const k of Object.keys(honoParams)) {
894
+ pathParams[k] = honoParams[k];
895
+ }
896
+ const cookies = parseCookieHeader(headers["cookie"]);
897
+ let bodyJson = void 0;
898
+ let bodyText = "";
899
+ const ct = headers["content-type"] ?? "";
900
+ if (ct.toLowerCase().includes("json")) {
901
+ try {
902
+ bodyText = await c.req.text();
903
+ bodyJson = bodyText ? JSON.parse(bodyText) : void 0;
904
+ } catch {
905
+ }
906
+ } else {
907
+ try {
908
+ bodyText = await c.req.text();
909
+ } catch {
910
+ }
911
+ }
912
+ return { query, pathParams, headers, cookies, bodyText, bodyJson };
913
+ }
914
+ function parseCookieHeader(cookieHeader) {
915
+ const out = /* @__PURE__ */ Object.create(null);
916
+ if (!cookieHeader) return out;
917
+ for (const part of cookieHeader.split(";")) {
918
+ const eq = part.indexOf("=");
919
+ if (eq === -1) continue;
920
+ const k = part.slice(0, eq).trim();
921
+ const v = part.slice(eq + 1).trim();
922
+ if (k && !(k in out)) out[k] = v;
923
+ }
924
+ return out;
925
+ }
926
+ async function respond(c, response, ctx) {
927
+ if (response.delayMs && response.delayMs > 0) {
928
+ await new Promise((resolve) => setTimeout(resolve, response.delayMs));
929
+ }
930
+ const finalResponse = applyMultipliers(response, ctx);
931
+ let userSetContentType = false;
932
+ for (const h of finalResponse.headers) {
933
+ if (!h.enabled) continue;
934
+ if (!h.key.trim()) continue;
935
+ if (h.key.toLowerCase() === "content-type") userSetContentType = true;
936
+ c.header(h.key, h.value);
937
+ }
938
+ const body = finalResponse.body;
939
+ if (!userSetContentType) {
940
+ const defaultCt = defaultContentTypeFor(body.type);
941
+ if (defaultCt) c.header("Content-Type", defaultCt);
942
+ }
943
+ c.header("X-Content-Type-Options", "nosniff");
944
+ if (body.type === "none") {
945
+ return c.body(null, finalResponse.status);
946
+ }
947
+ if (body.type === "binary" || body.type === "form-data") {
948
+ return c.body(null, finalResponse.status);
949
+ }
950
+ return c.body(body.content, finalResponse.status);
951
+ }
952
+ function defaultContentTypeFor(bodyType) {
953
+ switch (bodyType) {
954
+ case "json":
955
+ return "application/json; charset=utf-8";
956
+ case "text":
957
+ return "text/plain; charset=utf-8";
958
+ case "binary":
959
+ return "application/octet-stream";
960
+ case "form-data":
961
+ return "multipart/form-data";
962
+ case "none":
963
+ default:
964
+ return null;
965
+ }
966
+ }
967
+ function openApiPathToHono(path) {
968
+ return path.replace(/\{([^}]+)\}/g, ":$1");
969
+ }
970
+
971
+ // src/runtime/nodeAdapter.ts
972
+ var import_node_server = require("@hono/node-server");
973
+
974
+ // src/runtime/portFinder.ts
975
+ var import_node_net = require("net");
976
+ async function getFreePort() {
977
+ return new Promise((resolve, reject) => {
978
+ const server = (0, import_node_net.createServer)();
979
+ server.unref();
980
+ server.on("error", reject);
981
+ server.listen(0, () => {
982
+ const address = server.address();
983
+ if (typeof address === "object" && address !== null) {
984
+ const port = address.port;
985
+ server.close(() => resolve(port));
986
+ } else {
987
+ server.close(() => reject(new Error("Could not determine free port")));
988
+ }
989
+ });
990
+ });
991
+ }
992
+ async function isPortFree(port) {
993
+ return new Promise((resolve) => {
994
+ const server = (0, import_node_net.createServer)();
995
+ server.unref();
996
+ server.once("error", () => resolve(false));
997
+ server.once("listening", () => {
998
+ server.close(() => resolve(true));
999
+ });
1000
+ server.listen(port);
1001
+ });
1002
+ }
1003
+
1004
+ // src/runtime/nodeAdapter.ts
1005
+ async function serveOnNode(app, opts = {}) {
1006
+ const port = opts.port && opts.port > 0 ? opts.port : await getFreePort();
1007
+ const host = opts.host ?? "127.0.0.1";
1008
+ let server = null;
1009
+ await new Promise((resolve, reject) => {
1010
+ try {
1011
+ server = (0, import_node_server.serve)(
1012
+ {
1013
+ fetch: app.fetch,
1014
+ port,
1015
+ hostname: host
1016
+ },
1017
+ () => resolve()
1018
+ );
1019
+ server.on("error", reject);
1020
+ } catch (err) {
1021
+ reject(err instanceof Error ? err : new Error(String(err)));
1022
+ }
1023
+ });
1024
+ return {
1025
+ port,
1026
+ close: () => new Promise((resolve, reject) => {
1027
+ if (!server) return resolve();
1028
+ server.close((err) => {
1029
+ if (err) reject(err);
1030
+ else resolve();
1031
+ });
1032
+ })
1033
+ };
1034
+ }
1035
+
1036
+ // src/index.ts
1037
+ async function parseSourceToEndpoints(source) {
1038
+ switch (source.kind) {
1039
+ case "openapi":
1040
+ return parseOpenApiToEndpoints(source.spec, source.format);
1041
+ case "postman":
1042
+ return parsePostmanToEndpoints(source.collection);
1043
+ case "insomnia":
1044
+ return parseInsomniaToEndpoints(source.export);
1045
+ case "manual":
1046
+ return { endpoints: source.endpoints, warnings: [] };
1047
+ }
1048
+ }
1049
+ function createMockApp(server, opts = {}) {
1050
+ return buildRouter(server, opts);
1051
+ }
1052
+ async function startMockServer(server, opts = {}) {
1053
+ const app = createMockApp(server, { onRequest: opts.onRequest });
1054
+ const desired = {
1055
+ host: opts.host,
1056
+ port: opts.port ?? server.defaultPort ?? void 0
1057
+ };
1058
+ return serveOnNode(app, desired);
1059
+ }
1060
+ async function stopMockServer(handle) {
1061
+ return handle.close();
1062
+ }
1063
+ // Annotate the CommonJS export names for ESM import in node:
1064
+ 0 && (module.exports = {
1065
+ buildRouter,
1066
+ createMockApp,
1067
+ getFreePort,
1068
+ isPortFree,
1069
+ openApiPathToHono,
1070
+ parseInsomniaToEndpoints,
1071
+ parseOpenApiToEndpoints,
1072
+ parsePostmanToEndpoints,
1073
+ parseSourceToEndpoints,
1074
+ schemaToExample,
1075
+ startMockServer,
1076
+ stopMockServer
1077
+ });
1078
+ //# sourceMappingURL=index.cjs.map