@jaredwray/mockhttp 1.4.0 → 1.5.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/dist/index.mjs ADDED
@@ -0,0 +1,3492 @@
1
+ import process$1 from "node:process";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import path, { join } from "node:path";
4
+ import fastifyCookie from "@fastify/cookie";
5
+ import fastifyHelmet from "@fastify/helmet";
6
+ import fastifyRateLimit from "@fastify/rate-limit";
7
+ import fastifyStatic from "@fastify/static";
8
+ import { fastifySwagger } from "@fastify/swagger";
9
+ import { detect } from "detect-port";
10
+ import Fastify from "fastify";
11
+ import { Hookified } from "hookified";
12
+ import * as crypto$1 from "node:crypto";
13
+ import crypto, { randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
14
+ import { Buffer as Buffer$1 } from "node:buffer";
15
+ import { readFileSync } from "node:fs";
16
+ import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
17
+ import { escape } from "html-escaper";
18
+ import { fastifySwaggerUi } from "@fastify/swagger-ui";
19
+ //#region src/certificate.ts
20
+ function encodeLength(length) {
21
+ if (length < 128) return Buffer.from([length]);
22
+ const bytes = [];
23
+ let temp = length;
24
+ while (temp > 0) {
25
+ bytes.unshift(temp & 255);
26
+ temp >>= 8;
27
+ }
28
+ return Buffer.from([128 | bytes.length, ...bytes]);
29
+ }
30
+ function encodeTlv(tag, value) {
31
+ return Buffer.concat([
32
+ Buffer.from([tag]),
33
+ encodeLength(value.length),
34
+ value
35
+ ]);
36
+ }
37
+ function encodeSequence(...elements) {
38
+ return encodeTlv(48, Buffer.concat(elements));
39
+ }
40
+ function encodeSet(...elements) {
41
+ return encodeTlv(49, Buffer.concat(elements));
42
+ }
43
+ function encodeInteger(value) {
44
+ let buf;
45
+ if (typeof value === "number") if (value === 0) buf = Buffer.from([0]);
46
+ else {
47
+ const bytes = [];
48
+ let temp = value;
49
+ while (temp > 0) {
50
+ bytes.unshift(temp & 255);
51
+ temp >>= 8;
52
+ }
53
+ if (bytes[0] & 128) bytes.unshift(0);
54
+ buf = Buffer.from(bytes);
55
+ }
56
+ else if (value.length > 0 && value[0] & 128) buf = Buffer.concat([Buffer.from([0]), value]);
57
+ else buf = value;
58
+ return encodeTlv(2, buf);
59
+ }
60
+ function encodeBitString(value) {
61
+ return encodeTlv(3, Buffer.concat([Buffer.from([0]), value]));
62
+ }
63
+ function encodeOctetString(value) {
64
+ return encodeTlv(4, value);
65
+ }
66
+ function encodeOid(oid) {
67
+ const bytes = [];
68
+ bytes.push(40 * oid[0] + oid[1]);
69
+ for (let i = 2; i < oid.length; i++) {
70
+ let component = oid[i];
71
+ if (component < 128) bytes.push(component);
72
+ else {
73
+ const encoded = [];
74
+ encoded.push(component & 127);
75
+ component >>= 7;
76
+ while (component > 0) {
77
+ encoded.push(component & 127 | 128);
78
+ component >>= 7;
79
+ }
80
+ encoded.reverse();
81
+ bytes.push(...encoded);
82
+ }
83
+ }
84
+ return encodeTlv(6, Buffer.from(bytes));
85
+ }
86
+ function encodeUtf8String(value) {
87
+ return encodeTlv(12, Buffer.from(value, "utf8"));
88
+ }
89
+ /**
90
+ * Encode a date as ASN.1 time. Uses UTCTime (tag 0x17) for years 1950-2049
91
+ * and GeneralizedTime (tag 0x18) for years >= 2050, per RFC 5280 Section 4.1.2.5.
92
+ */
93
+ function encodeTime(date) {
94
+ const fullYear = date.getUTCFullYear();
95
+ const timePart = `${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
96
+ if (fullYear >= 2050) {
97
+ const str = `${fullYear}${timePart}`;
98
+ return encodeTlv(24, Buffer.from(str, "ascii"));
99
+ }
100
+ const str = `${pad2(fullYear % 100)}${timePart}`;
101
+ return encodeTlv(23, Buffer.from(str, "ascii"));
102
+ }
103
+ function encodeContextSpecific(tag, value) {
104
+ return encodeTlv(160 | tag, value);
105
+ }
106
+ function pad2(n) {
107
+ return n.toString().padStart(2, "0");
108
+ }
109
+ const OID_SHA256_WITH_RSA = [
110
+ 1,
111
+ 2,
112
+ 840,
113
+ 113549,
114
+ 1,
115
+ 1,
116
+ 11
117
+ ];
118
+ const OID_COMMON_NAME = [
119
+ 2,
120
+ 5,
121
+ 4,
122
+ 3
123
+ ];
124
+ const OID_SUBJECT_ALT_NAME = [
125
+ 2,
126
+ 5,
127
+ 29,
128
+ 17
129
+ ];
130
+ function buildAlgorithmIdentifier() {
131
+ return encodeSequence(encodeOid(OID_SHA256_WITH_RSA), encodeTlv(5, Buffer.alloc(0)));
132
+ }
133
+ function buildName(cn) {
134
+ return encodeSequence(encodeSet(encodeSequence(encodeOid(OID_COMMON_NAME), encodeUtf8String(cn))));
135
+ }
136
+ function buildValidity(notBefore, notAfter) {
137
+ return encodeSequence(encodeTime(notBefore), encodeTime(notAfter));
138
+ }
139
+ function encodeIpAddress(ip) {
140
+ if (ip.includes(".")) {
141
+ const parts = ip.split(".").map(Number);
142
+ return Buffer.from(parts);
143
+ }
144
+ const expanded = expandIpv6(ip);
145
+ const buf = Buffer.alloc(16);
146
+ const groups = expanded.split(":");
147
+ for (let i = 0; i < 8; i++) {
148
+ const val = Number.parseInt(groups[i], 16);
149
+ buf.writeUInt16BE(val, i * 2);
150
+ }
151
+ return buf;
152
+ }
153
+ function expandIpv6(ip) {
154
+ if (ip.includes("::")) {
155
+ const [left, right] = ip.split("::");
156
+ const leftGroups = left ? left.split(":") : [];
157
+ const rightGroups = right ? right.split(":") : [];
158
+ const missing = 8 - leftGroups.length - rightGroups.length;
159
+ const middle = Array.from({ length: missing }).fill("0000");
160
+ return [
161
+ ...leftGroups,
162
+ ...middle,
163
+ ...rightGroups
164
+ ].map((g) => g.padStart(4, "0")).join(":");
165
+ }
166
+ return ip.split(":").map((g) => g.padStart(4, "0")).join(":");
167
+ }
168
+ function buildSubjectAltNameExtension(altNames) {
169
+ const names = [];
170
+ for (const alt of altNames) if (alt.type === "dns") names.push(encodeTlv(130, Buffer.from(alt.value, "ascii")));
171
+ else names.push(encodeTlv(135, encodeIpAddress(alt.value)));
172
+ const sanValue = encodeSequence(...names);
173
+ return encodeSequence(encodeOid(OID_SUBJECT_ALT_NAME), encodeOctetString(sanValue));
174
+ }
175
+ function buildExtensions(altNames) {
176
+ return encodeContextSpecific(3, encodeSequence(buildSubjectAltNameExtension(altNames)));
177
+ }
178
+ function buildTbsCertificate(serialNumber, issuerCn, notBefore, notAfter, subjectCn, publicKeyDer, altNames) {
179
+ return encodeSequence(encodeContextSpecific(0, encodeInteger(2)), encodeInteger(serialNumber), buildAlgorithmIdentifier(), buildName(issuerCn), buildValidity(notBefore, notAfter), buildName(subjectCn), publicKeyDer, buildExtensions(altNames));
180
+ }
181
+ function derToPem(der, label) {
182
+ const base64 = der.toString("base64");
183
+ const lines = [];
184
+ for (let i = 0; i < base64.length; i += 64) lines.push(base64.slice(i, i + 64));
185
+ return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
186
+ }
187
+ /**
188
+ * Generate a self-signed certificate using only Node.js built-in crypto.
189
+ * Returns PEM-encoded certificate and private key strings.
190
+ */
191
+ function generateCertificate(options) {
192
+ const commonName = options?.commonName ?? "localhost";
193
+ const validityDays = options?.validityDays ?? 365;
194
+ const keySize = options?.keySize ?? 2048;
195
+ const altNames = options?.altNames ?? [
196
+ {
197
+ type: "dns",
198
+ value: "localhost"
199
+ },
200
+ {
201
+ type: "ip",
202
+ value: "127.0.0.1"
203
+ },
204
+ {
205
+ type: "ip",
206
+ value: "::1"
207
+ }
208
+ ];
209
+ const { publicKey, privateKey } = crypto$1.generateKeyPairSync("rsa", {
210
+ modulusLength: keySize,
211
+ publicKeyEncoding: {
212
+ type: "spki",
213
+ format: "der"
214
+ },
215
+ privateKeyEncoding: {
216
+ type: "pkcs8",
217
+ format: "pem"
218
+ }
219
+ });
220
+ const notBefore = /* @__PURE__ */ new Date();
221
+ const notAfter = /* @__PURE__ */ new Date();
222
+ notAfter.setDate(notAfter.getDate() + validityDays);
223
+ const serialBytes = crypto$1.randomBytes(16);
224
+ serialBytes[0] &= 127;
225
+ let start = 0;
226
+ /* v8 ignore start -- depends on random data producing leading zeros */
227
+ while (start < serialBytes.length - 1 && serialBytes[start] === 0) start++;
228
+ const tbsCertificate = buildTbsCertificate(serialBytes.subarray(start), commonName, notBefore, notAfter, commonName, publicKey, altNames);
229
+ const signer = crypto$1.createSign("SHA256");
230
+ signer.update(tbsCertificate);
231
+ const signature = signer.sign(privateKey);
232
+ return {
233
+ cert: derToPem(encodeSequence(tbsCertificate, buildAlgorithmIdentifier(), encodeBitString(signature)), "CERTIFICATE"),
234
+ key: privateKey
235
+ };
236
+ }
237
+ /**
238
+ * Generate a self-signed certificate and write PEM files to disk.
239
+ * Returns the same CertificateResult as generateCertificate().
240
+ */
241
+ async function generateCertificateFiles(options) {
242
+ const result = generateCertificate(options);
243
+ await fsPromises.writeFile(options.certPath, result.cert, "utf8");
244
+ await fsPromises.writeFile(options.keyPath, result.key, "utf8");
245
+ return result;
246
+ }
247
+ //#endregion
248
+ //#region src/fastify-config.ts
249
+ function getFastifyConfig(logging = true) {
250
+ return { logger: logging ? { transport: {
251
+ target: "pino-pretty",
252
+ options: {
253
+ colorize: true,
254
+ translateTime: true,
255
+ ignore: "pid,hostname",
256
+ singleLine: true
257
+ }
258
+ } } : false };
259
+ }
260
+ //#endregion
261
+ //#region src/routes/anything/index.ts
262
+ const anythingSchema = {
263
+ description: "Will return anything that you send it",
264
+ tags: ["Anything"],
265
+ response: { 200: {
266
+ type: "object",
267
+ properties: {
268
+ args: {
269
+ type: "object",
270
+ description: "Route parameters",
271
+ additionalProperties: true
272
+ },
273
+ data: {
274
+ type: "object",
275
+ description: "Request body data",
276
+ additionalProperties: true
277
+ },
278
+ files: {
279
+ type: "object",
280
+ description: "Uploaded files",
281
+ additionalProperties: true
282
+ },
283
+ form: {
284
+ type: "object",
285
+ description: "Form data",
286
+ additionalProperties: true
287
+ },
288
+ headers: {
289
+ type: "object",
290
+ description: "Request headers",
291
+ additionalProperties: true
292
+ },
293
+ json: {
294
+ type: "object",
295
+ description: "Parsed JSON body",
296
+ additionalProperties: true
297
+ },
298
+ method: {
299
+ type: "string",
300
+ description: "HTTP method used"
301
+ },
302
+ origin: {
303
+ type: "string",
304
+ description: "Origin of the request"
305
+ },
306
+ url: {
307
+ type: "string",
308
+ description: "Request URL"
309
+ }
310
+ },
311
+ additionalProperties: false
312
+ } }
313
+ };
314
+ const anythingRoute = (fastify) => {
315
+ const handler = async (request, reply) => {
316
+ const response = {
317
+ args: request.query,
318
+ data: request.body ?? {},
319
+ files: request.raw?.files ?? {},
320
+ form: request.body ?? {},
321
+ headers: request.headers,
322
+ json: request.body ?? {},
323
+ method: request.method,
324
+ origin: request.headers.origin ?? request.headers.host ?? "",
325
+ url: request.url
326
+ };
327
+ await reply.send(response);
328
+ };
329
+ const noValidation = {
330
+ schema: anythingSchema,
331
+ validatorCompiler: () => () => true
332
+ };
333
+ fastify.get("/anything", noValidation, handler);
334
+ fastify.post("/anything", noValidation, handler);
335
+ fastify.put("/anything", noValidation, handler);
336
+ fastify.patch("/anything", noValidation, handler);
337
+ fastify.delete("/anything", noValidation, handler);
338
+ };
339
+ //#endregion
340
+ //#region src/routes/auth/basic.ts
341
+ const parseBasic$1 = (header) => {
342
+ if (!header || typeof header !== "string") return;
343
+ const spaceIndex = header.indexOf(" ");
344
+ if (spaceIndex === -1) return;
345
+ const scheme = header.slice(0, spaceIndex);
346
+ const value = header.slice(spaceIndex + 1);
347
+ if (scheme.toLowerCase() !== "basic" || !value) return;
348
+ try {
349
+ const decoded = Buffer$1.from(value, "base64").toString("utf8");
350
+ const idx = decoded.indexOf(":");
351
+ if (idx === -1) return;
352
+ const username = decoded.slice(0, idx);
353
+ const password = decoded.slice(idx + 1);
354
+ if (username === "") return;
355
+ return {
356
+ username,
357
+ password
358
+ };
359
+ } catch {
360
+ /* v8 ignore next -- @preserve */
361
+ return;
362
+ }
363
+ };
364
+ const safeCompare = (a, b) => {
365
+ if (a.length !== b.length) return false;
366
+ try {
367
+ return timingSafeEqual(Buffer$1.from(a, "utf8"), Buffer$1.from(b, "utf8"));
368
+ } catch {
369
+ /* v8 ignore next -- @preserve */
370
+ return false;
371
+ }
372
+ };
373
+ const basicAuthSchema = {
374
+ description: "HTTP Basic authentication. Succeeds only if the user/pass provided in the path matches the Basic Authorization header.",
375
+ tags: ["Auth"],
376
+ params: {
377
+ type: "object",
378
+ properties: {
379
+ user: { type: "string" },
380
+ passwd: { type: "string" }
381
+ },
382
+ required: ["user", "passwd"]
383
+ },
384
+ response: {
385
+ 200: {
386
+ type: "object",
387
+ properties: {
388
+ authenticated: { type: "boolean" },
389
+ user: { type: "string" }
390
+ },
391
+ required: ["authenticated", "user"]
392
+ },
393
+ 401: {
394
+ type: "object",
395
+ properties: { message: { type: "string" } },
396
+ required: ["message"]
397
+ }
398
+ }
399
+ };
400
+ const basicAuthRoute = (fastify) => {
401
+ fastify.get("/basic-auth/:user/:passwd", { schema: basicAuthSchema }, async (request, reply) => {
402
+ const { user, passwd } = request.params;
403
+ if (!user || typeof user !== "string" || typeof passwd !== "string") {
404
+ reply.header("WWW-Authenticate", "Basic realm=\"mockhttp\"");
405
+ return reply.status(401).send({ message: "Unauthorized" });
406
+ }
407
+ const parsed = parseBasic$1(request.headers.authorization);
408
+ if (!parsed || !safeCompare(parsed.username, user) || !safeCompare(parsed.password, passwd)) {
409
+ reply.header("WWW-Authenticate", "Basic realm=\"mockhttp\"");
410
+ return reply.status(401).send({ message: "Unauthorized" });
411
+ }
412
+ return reply.send({
413
+ authenticated: true,
414
+ user
415
+ });
416
+ });
417
+ };
418
+ //#endregion
419
+ //#region src/routes/auth/bearer.ts
420
+ const parseBearer = (header) => {
421
+ if (!header) return;
422
+ const [scheme, token] = header.split(" ");
423
+ if (!scheme || scheme.toLowerCase() !== "bearer" || !token) return;
424
+ return token;
425
+ };
426
+ const bearerAuthSchema = {
427
+ description: "HTTP Bearer authentication. Succeeds if any Bearer token is present unless ?required=true is provided.",
428
+ tags: ["Auth"],
429
+ querystring: {
430
+ type: "object",
431
+ properties: { required: {
432
+ type: "boolean",
433
+ default: true
434
+ } }
435
+ },
436
+ response: {
437
+ 200: {
438
+ type: "object",
439
+ properties: {
440
+ authenticated: { type: "boolean" },
441
+ token: { type: "string" }
442
+ },
443
+ required: ["authenticated", "token"]
444
+ },
445
+ 401: {
446
+ type: "object",
447
+ properties: { message: { type: "string" } },
448
+ required: ["message"]
449
+ }
450
+ }
451
+ };
452
+ const bearerAuthRoute = (fastify) => {
453
+ fastify.get("/bearer", { schema: bearerAuthSchema }, async (request, reply) => {
454
+ const token = parseBearer(request.headers.authorization);
455
+ const required = request.query?.required ?? true;
456
+ if (!token && required) {
457
+ reply.header("WWW-Authenticate", "Bearer");
458
+ return reply.status(401).send({ message: "Unauthorized" });
459
+ }
460
+ if (!token) return reply.send({
461
+ authenticated: false,
462
+ token: ""
463
+ });
464
+ return reply.send({
465
+ authenticated: true,
466
+ token
467
+ });
468
+ });
469
+ };
470
+ //#endregion
471
+ //#region src/routes/auth/digest.ts
472
+ const makeNonce = () => crypto.randomBytes(16).toString("hex");
473
+ const algorithms = new Set([
474
+ "MD5",
475
+ "md5",
476
+ "SHA-256",
477
+ "sha-256",
478
+ "SHA-512",
479
+ "sha-512"
480
+ ]);
481
+ const hashAlg = (alg) => {
482
+ if (!alg) return "md5";
483
+ const a = alg.toLowerCase();
484
+ if (a === "md5") return "md5";
485
+ if (a === "sha-256") return "sha256";
486
+ if (a === "sha-512") return "sha512";
487
+ return "md5";
488
+ };
489
+ const h = (algo, data) => crypto.createHash(algo).update(data).digest("hex");
490
+ const parseDigest = (header) => {
491
+ if (!header) return;
492
+ const spaceIndex = header.indexOf(" ");
493
+ /* v8 ignore next -- @preserve */
494
+ if (spaceIndex === -1) return;
495
+ const scheme = header.slice(0, spaceIndex);
496
+ const rest = header.slice(spaceIndex + 1);
497
+ if (!scheme || scheme.toLowerCase() !== "digest" || !rest) return;
498
+ const out = {};
499
+ const parts = [];
500
+ let current = "";
501
+ let inQuotes = false;
502
+ for (const char of rest) if (char === "\"") {
503
+ inQuotes = !inQuotes;
504
+ current += char;
505
+ } else if (char === "," && !inQuotes) {
506
+ if (current.trim()) parts.push(current.trim());
507
+ current = "";
508
+ } else current += char;
509
+ if (current.trim()) parts.push(current.trim());
510
+ if (parts.length === 0) return out;
511
+ for (const p of parts) {
512
+ const equalIndex = p.indexOf("=");
513
+ if (equalIndex === -1) continue;
514
+ const k = p.slice(0, equalIndex).trim();
515
+ let value = p.slice(equalIndex + 1).trim();
516
+ if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
517
+ out[k] = value;
518
+ }
519
+ return out;
520
+ };
521
+ const digestAuthSchema = {
522
+ description: "HTTP Digest authentication. Mirrors httpbin behavior for testing clients.",
523
+ tags: ["Auth"],
524
+ params: {
525
+ type: "object",
526
+ properties: {
527
+ qop: { type: "string" },
528
+ user: { type: "string" },
529
+ passwd: { type: "string" },
530
+ algorithm: { type: "string" },
531
+ stale_after: { type: "string" }
532
+ },
533
+ required: [
534
+ "qop",
535
+ "user",
536
+ "passwd"
537
+ ]
538
+ }
539
+ };
540
+ const digestAuthRoute = (fastify) => {
541
+ fastify.get("/digest-auth/:qop/:user/:passwd", { schema: digestAuthSchema }, async (request, reply) => {
542
+ const { user, passwd } = request.params;
543
+ const realm = "mockhttp";
544
+ const nonce = makeNonce();
545
+ const auth = parseDigest(request.headers.authorization);
546
+ if (!auth) {
547
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", nonce="${nonce}", opaque="${makeNonce()}"`);
548
+ return reply.status(401).send({ message: "Unauthorized" });
549
+ }
550
+ if (auth.username !== user) {
551
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", nonce="${nonce}", opaque="${makeNonce()}"`);
552
+ return reply.status(401).send({ message: "Unauthorized" });
553
+ }
554
+ const method = "GET";
555
+ const uri = auth.uri ?? request.url.replace(/^https?:\/\/[^/]+/, "");
556
+ const algo = hashAlg(auth.algorithm);
557
+ const ha1 = h(algo, `${user}:${realm}:${passwd}`);
558
+ const ha2 = h(algo, `${method}:${uri}`);
559
+ let expected = "";
560
+ expected = auth.qop ? h(algo, `${ha1}:${auth.nonce}:${auth.nc}:${auth.cnonce}:${auth.qop}:${ha2}`) : h(algo, `${ha1}:${auth.nonce}:${ha2}`);
561
+ if (!auth.response || auth.response !== expected) {
562
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", nonce="${nonce}", opaque="${makeNonce()}"`);
563
+ return reply.status(401).send({ message: "Unauthorized" });
564
+ }
565
+ return reply.send({
566
+ authenticated: true,
567
+ user
568
+ });
569
+ });
570
+ fastify.get("/digest-auth/:qop/:user/:passwd/:algorithm", { schema: digestAuthSchema }, async (request, reply) => {
571
+ const { user, passwd, algorithm } = request.params;
572
+ const realm = "mockhttp";
573
+ const algoHeader = algorithms.has(String(algorithm)) ? String(algorithm) : "MD5";
574
+ const nonce = makeNonce();
575
+ const auth = parseDigest(request.headers.authorization);
576
+ if (!auth) {
577
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", algorithm=${algoHeader}, nonce="${nonce}", opaque="${makeNonce()}"`);
578
+ return reply.status(401).send({ message: "Unauthorized" });
579
+ }
580
+ if (auth.username !== user) {
581
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", algorithm=${algoHeader}, nonce="${nonce}", opaque="${makeNonce()}"`);
582
+ return reply.status(401).send({ message: "Unauthorized" });
583
+ }
584
+ const method = "GET";
585
+ const uri = auth.uri ?? request.url.replace(/^https?:\/\/[^/]+/, "");
586
+ const algo = hashAlg(auth.algorithm ?? algoHeader);
587
+ const ha1 = h(algo, `${user}:${realm}:${passwd}`);
588
+ const ha2 = h(algo, `${method}:${uri}`);
589
+ const expected = auth.qop ? h(algo, `${ha1}:${auth.nonce}:${auth.nc}:${auth.cnonce}:${auth.qop}:${ha2}`) : h(algo, `${ha1}:${auth.nonce}:${ha2}`);
590
+ if (!auth.response || auth.response !== expected) {
591
+ reply.header("WWW-Authenticate", `Digest realm="${realm}", qop="auth,auth-int", algorithm=${algoHeader}, nonce="${nonce}", opaque="${makeNonce()}"`);
592
+ return reply.status(401).send({ message: "Unauthorized" });
593
+ }
594
+ return reply.send({
595
+ authenticated: true,
596
+ user
597
+ });
598
+ });
599
+ };
600
+ //#endregion
601
+ //#region src/routes/auth/hidden-basic.ts
602
+ const parseBasic = (header) => {
603
+ if (!header) return;
604
+ const [scheme, value] = header.split(" ");
605
+ if (!scheme || scheme.toLowerCase() !== "basic" || !value) return;
606
+ try {
607
+ const decoded = Buffer$1.from(value, "base64").toString("utf8");
608
+ const idx = decoded.indexOf(":");
609
+ if (idx === -1) return;
610
+ return {
611
+ username: decoded.slice(0, idx),
612
+ password: decoded.slice(idx + 1)
613
+ };
614
+ } catch {
615
+ /* v8 ignore next -- @preserve */
616
+ return;
617
+ }
618
+ };
619
+ const hiddenBasicAuthSchema = {
620
+ description: "HTTP Basic authentication with docs hidden. Mirrors /basic-auth but hides from schema.",
621
+ tags: ["Auth"],
622
+ hide: true,
623
+ params: {
624
+ type: "object",
625
+ properties: {
626
+ user: { type: "string" },
627
+ passwd: { type: "string" }
628
+ },
629
+ required: ["user", "passwd"]
630
+ }
631
+ };
632
+ const hiddenBasicAuthRoute = (fastify) => {
633
+ fastify.get("/hidden-basic-auth/:user/:passwd", { schema: hiddenBasicAuthSchema }, async (request, reply) => {
634
+ const { user, passwd } = request.params;
635
+ const parsed = parseBasic(request.headers.authorization);
636
+ if (!parsed || parsed.username !== user || parsed.password !== passwd) {
637
+ reply.header("WWW-Authenticate", "Basic realm=\"mockhttp\"");
638
+ return reply.status(401).send({ message: "Unauthorized" });
639
+ }
640
+ return reply.send({
641
+ authenticated: true,
642
+ user
643
+ });
644
+ });
645
+ };
646
+ //#endregion
647
+ //#region src/routes/cookies/delete.ts
648
+ const cookiesDeleteSchema = {
649
+ description: "Delete a cookie",
650
+ tags: ["Cookies"],
651
+ querystring: {
652
+ type: "object",
653
+ properties: { name: { type: "string" } },
654
+ required: ["name"],
655
+ additionalProperties: false
656
+ },
657
+ response: { 204: { type: "null" } }
658
+ };
659
+ const deleteCookieRoute = (fastify) => {
660
+ fastify.delete("/cookies", { schema: cookiesDeleteSchema }, async (request, reply) => {
661
+ const { name } = request.query;
662
+ reply.clearCookie(name, { path: "/" });
663
+ return reply.status(204).send();
664
+ });
665
+ };
666
+ //#endregion
667
+ //#region src/routes/cookies/get.ts
668
+ const getCookiesRouteSchema = {
669
+ description: "Return cookies from the request",
670
+ tags: ["Cookies"],
671
+ response: { 200: {
672
+ type: "object",
673
+ properties: { cookies: {
674
+ type: "object",
675
+ additionalProperties: { type: "string" }
676
+ } },
677
+ required: ["cookies"]
678
+ } }
679
+ };
680
+ const getCookiesRoute = (fastify) => {
681
+ fastify.get("/cookies", { schema: getCookiesRouteSchema }, async (request, reply) => {
682
+ reply.type("application/json");
683
+ await reply.send({ cookies: request.cookies });
684
+ });
685
+ };
686
+ //#endregion
687
+ //#region src/routes/cookies/post.ts
688
+ const postCookieRouteSchema = {
689
+ description: "Set a cookie",
690
+ tags: ["Cookies"],
691
+ body: {
692
+ type: "object",
693
+ properties: {
694
+ name: { type: "string" },
695
+ value: { type: "string" },
696
+ expires: {
697
+ type: "string",
698
+ format: "date-time"
699
+ }
700
+ },
701
+ required: ["name", "value"],
702
+ additionalProperties: false
703
+ },
704
+ response: { 200: {
705
+ type: "object",
706
+ properties: { cookies: {
707
+ type: "object",
708
+ additionalProperties: { type: "string" }
709
+ } },
710
+ required: ["cookies"]
711
+ } }
712
+ };
713
+ const postCookieRoute = (fastify) => {
714
+ fastify.post("/cookies", { schema: postCookieRouteSchema }, async (request, reply) => {
715
+ const { name, value, expires } = request.body;
716
+ if (expires) {
717
+ const date = new Date(expires);
718
+ reply.setCookie(name, value, {
719
+ path: "/",
720
+ expires: date
721
+ });
722
+ } else reply.setCookie(name, value, { path: "/" });
723
+ return reply.status(200).send();
724
+ });
725
+ };
726
+ //#endregion
727
+ //#region src/routes/dynamic-data/base64.ts
728
+ const base64Schema = {
729
+ description: "Decodes base64url-encoded string",
730
+ tags: ["Dynamic Data"],
731
+ params: {
732
+ type: "object",
733
+ properties: { value: {
734
+ type: "string",
735
+ description: "Base64 encoded value to decode"
736
+ } },
737
+ required: ["value"]
738
+ },
739
+ response: {
740
+ 200: {
741
+ type: "string",
742
+ description: "Decoded value"
743
+ },
744
+ 400: {
745
+ type: "object",
746
+ properties: { error: { type: "string" } }
747
+ }
748
+ }
749
+ };
750
+ function isValidBase64(str) {
751
+ if (!/^[A-Za-z0-9+/\-_]*={0,2}$/.test(str)) return false;
752
+ return str.replace(/=+$/, "").length % 4 !== 1;
753
+ }
754
+ const base64Route = (fastify) => {
755
+ fastify.get("/base64/:value", { schema: base64Schema }, async (request, reply) => {
756
+ const { value } = request.params;
757
+ if (!isValidBase64(value)) return reply.code(400).send({ error: "Incorrect Base64 data" });
758
+ const normalizedValue = value.replace(/-/g, "+").replace(/_/g, "/");
759
+ const decoded = Buffer.from(normalizedValue, "base64").toString("utf-8");
760
+ return reply.header("Content-Type", "text/html; charset=utf-8").send(decoded);
761
+ });
762
+ };
763
+ //#endregion
764
+ //#region src/routes/dynamic-data/bytes.ts
765
+ const bytesSchema = {
766
+ description: "Returns n random bytes generated with given seed",
767
+ tags: ["Dynamic Data"],
768
+ params: {
769
+ type: "object",
770
+ properties: { n: {
771
+ type: "string",
772
+ description: "Number of bytes to generate"
773
+ } },
774
+ required: ["n"]
775
+ },
776
+ querystring: {
777
+ type: "object",
778
+ properties: { seed: {
779
+ type: "string",
780
+ description: "Seed for random number generation"
781
+ } }
782
+ },
783
+ response: {
784
+ 200: {
785
+ type: "string",
786
+ description: "Random binary data"
787
+ },
788
+ 400: {
789
+ type: "object",
790
+ properties: { error: { type: "string" } }
791
+ }
792
+ }
793
+ };
794
+ function seededRandom$1(seed) {
795
+ let state = seed;
796
+ return () => {
797
+ state = state * 1103515245 + 12345 & 2147483647;
798
+ return state / 2147483647;
799
+ };
800
+ }
801
+ function generateSeededBytes$1(n, seed) {
802
+ const random = seededRandom$1(seed);
803
+ const bytes = Buffer.alloc(n);
804
+ for (let i = 0; i < n; i++) bytes[i] = Math.floor(random() * 256);
805
+ return bytes;
806
+ }
807
+ const MAX_BYTES$3 = 100 * 1024;
808
+ const bytesRoute = (fastify) => {
809
+ fastify.get("/bytes/:n", { schema: bytesSchema }, async (request, reply) => {
810
+ const n = Number.parseInt(request.params.n, 10);
811
+ if (Number.isNaN(n) || n < 0) return reply.code(400).send({ error: "n must be a non-negative integer" });
812
+ const limitedN = Math.min(n, MAX_BYTES$3);
813
+ const seedParam = request.query.seed;
814
+ let bytes;
815
+ if (seedParam !== void 0) {
816
+ const seed = Number.parseInt(seedParam, 10);
817
+ if (Number.isNaN(seed)) return reply.code(400).send({ error: "seed must be an integer" });
818
+ bytes = generateSeededBytes$1(limitedN, seed);
819
+ } else bytes = randomBytes(limitedN);
820
+ return reply.header("Content-Type", "application/octet-stream").header("Content-Length", bytes.length).send(bytes);
821
+ });
822
+ };
823
+ //#endregion
824
+ //#region src/routes/dynamic-data/delay.ts
825
+ const delaySchema = {
826
+ description: "Returns a delayed response (max of 10 seconds)",
827
+ tags: ["Dynamic Data"],
828
+ params: {
829
+ type: "object",
830
+ properties: { delay: {
831
+ type: "string",
832
+ description: "Delay in seconds (max 10)"
833
+ } },
834
+ required: ["delay"]
835
+ },
836
+ querystring: {
837
+ type: "object",
838
+ additionalProperties: true
839
+ },
840
+ response: {
841
+ 200: {
842
+ type: "object",
843
+ properties: {
844
+ args: { type: "object" },
845
+ data: { type: "string" },
846
+ files: { type: "object" },
847
+ form: { type: "object" },
848
+ headers: { type: "object" },
849
+ origin: { type: "string" },
850
+ url: { type: "string" }
851
+ }
852
+ },
853
+ 400: {
854
+ type: "object",
855
+ properties: { error: { type: "string" } }
856
+ }
857
+ }
858
+ };
859
+ const MAX_DELAY$1 = 10;
860
+ function sleep$2(ms) {
861
+ return new Promise((resolve) => setTimeout(resolve, ms));
862
+ }
863
+ const delayRoute = (fastify) => {
864
+ const handler = async (request, reply) => {
865
+ const delay = Number.parseFloat(request.params.delay);
866
+ if (Number.isNaN(delay) || delay < 0) return reply.code(400).send({ error: "delay must be a non-negative number" });
867
+ await sleep$2(Math.min(delay, MAX_DELAY$1) * 1e3);
868
+ const { protocol } = request;
869
+ /* v8 ignore next -- @preserve */
870
+ const host = request.headers.host || "localhost";
871
+ return {
872
+ args: request.query || {},
873
+ data: "",
874
+ files: {},
875
+ form: {},
876
+ headers: request.headers,
877
+ origin: request.ip,
878
+ url: `${protocol}://${host}${request.url}`
879
+ };
880
+ };
881
+ fastify.get("/delay/:delay", { schema: delaySchema }, handler);
882
+ fastify.post("/delay/:delay", { schema: delaySchema }, handler);
883
+ fastify.put("/delay/:delay", { schema: delaySchema }, handler);
884
+ fastify.patch("/delay/:delay", { schema: delaySchema }, handler);
885
+ fastify.delete("/delay/:delay", { schema: delaySchema }, handler);
886
+ };
887
+ //#endregion
888
+ //#region src/routes/dynamic-data/drip.ts
889
+ const dripSchema = {
890
+ description: "Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code",
891
+ tags: ["Dynamic Data"],
892
+ querystring: {
893
+ type: "object",
894
+ properties: {
895
+ duration: {
896
+ type: "string",
897
+ description: "Duration in seconds over which to drip the data (default: 2)"
898
+ },
899
+ numbytes: {
900
+ type: "string",
901
+ description: "Number of bytes to send (default: 10)"
902
+ },
903
+ code: {
904
+ type: "string",
905
+ description: "HTTP status code to return (default: 200)"
906
+ },
907
+ delay: {
908
+ type: "string",
909
+ description: "Initial delay in seconds before starting (default: 2)"
910
+ }
911
+ }
912
+ },
913
+ response: {
914
+ 200: {
915
+ type: "string",
916
+ description: "Dripped data"
917
+ },
918
+ 400: {
919
+ type: "object",
920
+ properties: { error: { type: "string" } }
921
+ }
922
+ }
923
+ };
924
+ function sleep$1(ms) {
925
+ return new Promise((resolve) => setTimeout(resolve, ms));
926
+ }
927
+ const DEFAULT_DURATION = 2;
928
+ const DEFAULT_NUMBYTES = 10;
929
+ const DEFAULT_CODE = 200;
930
+ const DEFAULT_DELAY = 2;
931
+ const MAX_DURATION = 10;
932
+ const MAX_DELAY = 10;
933
+ const MAX_BYTES$2 = 10 * 1024 * 1024;
934
+ const dripRoute = (fastify) => {
935
+ fastify.get("/drip", { schema: dripSchema }, async (request, reply) => {
936
+ /* v8 ignore next 2 -- @preserve */
937
+ const duration = request.query.duration ? Number.parseFloat(request.query.duration) : DEFAULT_DURATION;
938
+ const numbytes = request.query.numbytes ? Number.parseInt(request.query.numbytes, 10) : DEFAULT_NUMBYTES;
939
+ /* v8 ignore next 2 -- @preserve */
940
+ const code = request.query.code ? Number.parseInt(request.query.code, 10) : DEFAULT_CODE;
941
+ /* v8 ignore next 3 -- @preserve */
942
+ const delay = request.query.delay ? Number.parseFloat(request.query.delay) : DEFAULT_DELAY;
943
+ if (Number.isNaN(duration) || duration < 0) return reply.code(400).send({ error: "duration must be a non-negative number" });
944
+ if (Number.isNaN(numbytes) || numbytes < 0) return reply.code(400).send({ error: "numbytes must be a non-negative integer" });
945
+ if (Number.isNaN(code) || code < 100 || code > 599) return reply.code(400).send({ error: "code must be a valid HTTP status code" });
946
+ if (Number.isNaN(delay) || delay < 0) return reply.code(400).send({ error: "delay must be a non-negative number" });
947
+ const actualDuration = Math.min(duration, MAX_DURATION);
948
+ const actualDelay = Math.min(delay, MAX_DELAY);
949
+ const actualBytes = Math.min(numbytes, MAX_BYTES$2);
950
+ if (actualDelay > 0) await sleep$1(actualDelay * 1e3);
951
+ reply.raw.writeHead(code, {
952
+ "Content-Type": "application/octet-stream",
953
+ "Content-Length": actualBytes.toString()
954
+ });
955
+ if (actualBytes === 0) {
956
+ reply.raw.end();
957
+ return;
958
+ }
959
+ const intervalMs = actualBytes > 1 ? actualDuration * 1e3 / (actualBytes - 1) : 0;
960
+ const byte = Buffer.from("*");
961
+ for (let i = 0; i < actualBytes; i++) {
962
+ reply.raw.write(byte);
963
+ if (i < actualBytes - 1 && intervalMs > 0) await sleep$1(intervalMs);
964
+ }
965
+ reply.raw.end();
966
+ });
967
+ };
968
+ //#endregion
969
+ //#region src/routes/dynamic-data/links.ts
970
+ const linksSchema = {
971
+ description: "Generate a page containing n links to other pages which do the same",
972
+ tags: ["Dynamic Data"],
973
+ params: {
974
+ type: "object",
975
+ properties: {
976
+ n: {
977
+ type: "string",
978
+ description: "Number of links to generate"
979
+ },
980
+ offset: {
981
+ type: "string",
982
+ description: "Starting offset for pagination (default: 0)"
983
+ }
984
+ },
985
+ required: ["n"]
986
+ },
987
+ response: {
988
+ 200: {
989
+ type: "string",
990
+ description: "HTML page with links"
991
+ },
992
+ 400: {
993
+ type: "object",
994
+ properties: { error: { type: "string" } }
995
+ }
996
+ }
997
+ };
998
+ const MAX_LINKS = 200;
999
+ const linksRoute = (fastify) => {
1000
+ fastify.get("/links/:n/:offset?", { schema: linksSchema }, async (request, reply) => {
1001
+ const n = Number.parseInt(request.params.n, 10);
1002
+ const offset = request.params.offset ? Number.parseInt(request.params.offset, 10) : 0;
1003
+ if (Number.isNaN(n) || n < 0) return reply.code(400).send({ error: "n must be a non-negative integer" });
1004
+ if (Number.isNaN(offset) || offset < 0) return reply.code(400).send({ error: "offset must be a non-negative integer" });
1005
+ const limitedN = Math.min(n, MAX_LINKS);
1006
+ const links = [];
1007
+ for (let i = 0; i < limitedN; i++) if (i === offset) links.push(`${i} `);
1008
+ else links.push(`<a href="/links/${limitedN}/${i}">${i}</a> `);
1009
+ const html = `<html>
1010
+ <head>
1011
+ <title>Links</title>
1012
+ </head>
1013
+ <body>
1014
+ ${links.join("")}
1015
+ </body>
1016
+ </html>`;
1017
+ return reply.header("Content-Type", "text/html; charset=utf-8").send(html);
1018
+ });
1019
+ };
1020
+ //#endregion
1021
+ //#region src/routes/dynamic-data/range.ts
1022
+ const rangeSchema = {
1023
+ description: "Streams n random bytes generated, and supports HTTP Range requests",
1024
+ tags: ["Dynamic Data"],
1025
+ params: {
1026
+ type: "object",
1027
+ properties: { numbytes: {
1028
+ type: "string",
1029
+ description: "Number of bytes available"
1030
+ } },
1031
+ required: ["numbytes"]
1032
+ },
1033
+ querystring: {
1034
+ type: "object",
1035
+ properties: { duration: {
1036
+ type: "string",
1037
+ description: "Delay before sending response in seconds"
1038
+ } }
1039
+ },
1040
+ response: {
1041
+ 200: {
1042
+ type: "string",
1043
+ description: "Binary data"
1044
+ },
1045
+ 206: {
1046
+ type: "string",
1047
+ description: "Partial binary data"
1048
+ },
1049
+ 400: {
1050
+ type: "object",
1051
+ properties: { error: { type: "string" } }
1052
+ },
1053
+ 416: {
1054
+ type: "object",
1055
+ properties: { error: { type: "string" } }
1056
+ }
1057
+ }
1058
+ };
1059
+ const MAX_BYTES$1 = 100 * 1024;
1060
+ function sleep(ms) {
1061
+ return new Promise((resolve) => setTimeout(resolve, ms));
1062
+ }
1063
+ function generateBytes(n) {
1064
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
1065
+ const bytes = Buffer.alloc(n);
1066
+ for (let i = 0; i < n; i++) bytes[i] = alphabet.charCodeAt(i % 26);
1067
+ return bytes;
1068
+ }
1069
+ function parseRangeHeader(rangeHeader, totalSize) {
1070
+ const match = rangeHeader.match(/^bytes=(.+)$/);
1071
+ if (!match) return null;
1072
+ const ranges = [];
1073
+ const parts = match[1].split(",");
1074
+ for (const part of parts) {
1075
+ const trimmedPart = part.trim();
1076
+ if (trimmedPart.startsWith("-")) {
1077
+ const suffix = Number.parseInt(trimmedPart.slice(1), 10);
1078
+ /* v8 ignore next -- @preserve */
1079
+ if (Number.isNaN(suffix)) return null;
1080
+ ranges.push({
1081
+ start: Math.max(0, totalSize - suffix),
1082
+ end: totalSize - 1
1083
+ });
1084
+ } else if (trimmedPart.endsWith("-")) {
1085
+ const start = Number.parseInt(trimmedPart.slice(0, -1), 10);
1086
+ /* v8 ignore next -- @preserve */
1087
+ if (Number.isNaN(start)) return null;
1088
+ ranges.push({
1089
+ start,
1090
+ end: totalSize - 1
1091
+ });
1092
+ } else {
1093
+ const [startStr, endStr] = trimmedPart.split("-");
1094
+ const start = Number.parseInt(startStr, 10);
1095
+ const end = Number.parseInt(endStr, 10);
1096
+ /* v8 ignore next -- @preserve */
1097
+ if (Number.isNaN(start) || Number.isNaN(end)) return null;
1098
+ ranges.push({
1099
+ start,
1100
+ end: Math.min(end, totalSize - 1)
1101
+ });
1102
+ }
1103
+ }
1104
+ for (const range of ranges) if (range.start > range.end || range.start >= totalSize) return null;
1105
+ return ranges;
1106
+ }
1107
+ const rangeRoute = (fastify) => {
1108
+ fastify.get("/range/:numbytes", { schema: rangeSchema }, async (request, reply) => {
1109
+ const numbytes = Number.parseInt(request.params.numbytes, 10);
1110
+ if (Number.isNaN(numbytes) || numbytes < 0) return reply.code(400).send({ error: "numbytes must be a non-negative integer" });
1111
+ const limitedBytes = Math.min(numbytes, MAX_BYTES$1);
1112
+ if (request.query.duration) {
1113
+ const duration = Number.parseFloat(request.query.duration);
1114
+ if (!Number.isNaN(duration) && duration > 0) await sleep(Math.min(duration, 10) * 1e3);
1115
+ }
1116
+ const allBytes = generateBytes(limitedBytes);
1117
+ const rangeHeader = request.headers.range;
1118
+ if (!rangeHeader) return reply.header("Content-Type", "application/octet-stream").header("Content-Length", allBytes.length).header("Accept-Ranges", "bytes").send(allBytes);
1119
+ const ranges = parseRangeHeader(rangeHeader, limitedBytes);
1120
+ if (!ranges || ranges.length === 0) return reply.code(416).header("Content-Range", `bytes */${limitedBytes}`).send({ error: "Range Not Satisfiable" });
1121
+ if (ranges.length === 1) {
1122
+ const range = ranges[0];
1123
+ const chunk = allBytes.subarray(range.start, range.end + 1);
1124
+ return reply.code(206).header("Content-Type", "application/octet-stream").header("Content-Length", chunk.length).header("Accept-Ranges", "bytes").header("Content-Range", `bytes ${range.start}-${range.end}/${limitedBytes}`).send(chunk);
1125
+ }
1126
+ const boundary = "3d6b6a416f9b5";
1127
+ const parts = [];
1128
+ for (const range of ranges) {
1129
+ const chunk = allBytes.subarray(range.start, range.end + 1);
1130
+ const header = `\r\n--${boundary}\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes ${range.start}-${range.end}/${limitedBytes}\r\n\r\n`;
1131
+ parts.push(Buffer.from(header));
1132
+ parts.push(chunk);
1133
+ }
1134
+ parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
1135
+ const multipartBody = Buffer.concat(parts);
1136
+ return reply.code(206).header("Content-Type", `multipart/byteranges; boundary=${boundary}`).header("Content-Length", multipartBody.length).send(multipartBody);
1137
+ });
1138
+ };
1139
+ //#endregion
1140
+ //#region src/routes/dynamic-data/stream.ts
1141
+ const streamSchema = {
1142
+ description: "Stream n JSON responses",
1143
+ tags: ["Dynamic Data"],
1144
+ params: {
1145
+ type: "object",
1146
+ properties: { n: {
1147
+ type: "string",
1148
+ description: "Number of JSON responses to stream (max 100)"
1149
+ } },
1150
+ required: ["n"]
1151
+ },
1152
+ response: {
1153
+ 200: {
1154
+ type: "string",
1155
+ description: "Newline-delimited JSON responses"
1156
+ },
1157
+ 400: {
1158
+ type: "object",
1159
+ properties: { error: { type: "string" } }
1160
+ }
1161
+ }
1162
+ };
1163
+ const MAX_LINES = 100;
1164
+ const streamRoute = (fastify) => {
1165
+ fastify.get("/stream/:n", { schema: streamSchema }, async (request, reply) => {
1166
+ const n = Number.parseInt(request.params.n, 10);
1167
+ if (Number.isNaN(n) || n < 0) return reply.code(400).send({ error: "n must be a non-negative integer" });
1168
+ const lines = Math.min(n, MAX_LINES);
1169
+ const { protocol } = request;
1170
+ /* v8 ignore next -- @preserve */
1171
+ const host = request.headers.host || "localhost";
1172
+ reply.raw.writeHead(200, {
1173
+ "Content-Type": "application/json",
1174
+ "Transfer-Encoding": "chunked"
1175
+ });
1176
+ for (let i = 0; i < lines; i++) {
1177
+ const data = {
1178
+ id: i,
1179
+ args: request.query || {},
1180
+ headers: request.headers,
1181
+ origin: request.ip,
1182
+ url: `${protocol}://${host}${request.url}`
1183
+ };
1184
+ reply.raw.write(`${JSON.stringify(data)}\n`);
1185
+ }
1186
+ reply.raw.end();
1187
+ });
1188
+ };
1189
+ //#endregion
1190
+ //#region src/routes/dynamic-data/stream-bytes.ts
1191
+ const streamBytesSchema = {
1192
+ description: "Streams n random bytes generated with given seed, in chunks of chunk_size per packet",
1193
+ tags: ["Dynamic Data"],
1194
+ params: {
1195
+ type: "object",
1196
+ properties: { n: {
1197
+ type: "string",
1198
+ description: "Number of bytes to stream"
1199
+ } },
1200
+ required: ["n"]
1201
+ },
1202
+ querystring: {
1203
+ type: "object",
1204
+ properties: {
1205
+ seed: {
1206
+ type: "string",
1207
+ description: "Seed for random number generation"
1208
+ },
1209
+ chunk_size: {
1210
+ type: "string",
1211
+ description: "Size of each chunk (default: 10240)"
1212
+ }
1213
+ }
1214
+ },
1215
+ response: {
1216
+ 200: {
1217
+ type: "string",
1218
+ description: "Streamed random binary data"
1219
+ },
1220
+ 400: {
1221
+ type: "object",
1222
+ properties: { error: { type: "string" } }
1223
+ }
1224
+ }
1225
+ };
1226
+ function seededRandom(seed) {
1227
+ let state = seed;
1228
+ return () => {
1229
+ state = state * 1103515245 + 12345 & 2147483647;
1230
+ return state / 2147483647;
1231
+ };
1232
+ }
1233
+ function generateSeededBytes(n, seed) {
1234
+ const random = seededRandom(seed);
1235
+ const bytes = Buffer.alloc(n);
1236
+ for (let i = 0; i < n; i++) bytes[i] = Math.floor(random() * 256);
1237
+ return bytes;
1238
+ }
1239
+ const MAX_BYTES = 100 * 1024;
1240
+ const DEFAULT_CHUNK_SIZE = 10240;
1241
+ const streamBytesRoute = (fastify) => {
1242
+ fastify.get("/stream-bytes/:n", { schema: streamBytesSchema }, async (request, reply) => {
1243
+ const n = Number.parseInt(request.params.n, 10);
1244
+ if (Number.isNaN(n) || n < 0) return reply.code(400).send({ error: "n must be a non-negative integer" });
1245
+ const limitedN = Math.min(n, MAX_BYTES);
1246
+ const chunkSize = request.query.chunk_size ? Number.parseInt(request.query.chunk_size, 10) : DEFAULT_CHUNK_SIZE;
1247
+ if (Number.isNaN(chunkSize) || chunkSize <= 0) return reply.code(400).send({ error: "chunk_size must be a positive integer" });
1248
+ const seedParam = request.query.seed;
1249
+ let allBytes;
1250
+ if (seedParam !== void 0) {
1251
+ const seed = Number.parseInt(seedParam, 10);
1252
+ if (Number.isNaN(seed)) return reply.code(400).send({ error: "seed must be an integer" });
1253
+ allBytes = generateSeededBytes(limitedN, seed);
1254
+ } else allBytes = randomBytes(limitedN);
1255
+ reply.raw.writeHead(200, {
1256
+ "Content-Type": "application/octet-stream",
1257
+ "Transfer-Encoding": "chunked"
1258
+ });
1259
+ let offset = 0;
1260
+ while (offset < allBytes.length) {
1261
+ const chunk = allBytes.subarray(offset, offset + chunkSize);
1262
+ reply.raw.write(chunk);
1263
+ offset += chunkSize;
1264
+ }
1265
+ reply.raw.end();
1266
+ });
1267
+ };
1268
+ //#endregion
1269
+ //#region src/routes/dynamic-data/uuid.ts
1270
+ const uuidSchema = {
1271
+ description: "Return a UUID4",
1272
+ tags: ["Dynamic Data"],
1273
+ response: { 200: {
1274
+ type: "object",
1275
+ properties: { uuid: {
1276
+ type: "string",
1277
+ format: "uuid"
1278
+ } },
1279
+ required: ["uuid"]
1280
+ } }
1281
+ };
1282
+ const uuidRoute = (fastify) => {
1283
+ fastify.get("/uuid", { schema: uuidSchema }, async () => ({ uuid: randomUUID() }));
1284
+ };
1285
+ //#endregion
1286
+ //#region src/routes/http-methods/delete.ts
1287
+ const deleteSchema = {
1288
+ description: "Handles a DELETE request and returns request information",
1289
+ tags: ["HTTP Methods"],
1290
+ response: { 200: {
1291
+ type: "object",
1292
+ properties: {
1293
+ method: { type: "string" },
1294
+ headers: {
1295
+ type: "object",
1296
+ additionalProperties: { type: "string" }
1297
+ },
1298
+ body: { oneOf: [
1299
+ {
1300
+ type: "object",
1301
+ additionalProperties: true
1302
+ },
1303
+ { type: "string" },
1304
+ { type: "null" }
1305
+ ] }
1306
+ },
1307
+ required: ["method", "headers"]
1308
+ } }
1309
+ };
1310
+ const deleteRoute = (fastify) => {
1311
+ fastify.delete("/delete", { schema: deleteSchema }, async (request) => ({
1312
+ method: "DELETE",
1313
+ headers: request.headers,
1314
+ body: request.body
1315
+ }));
1316
+ };
1317
+ //#endregion
1318
+ //#region src/routes/http-methods/get.ts
1319
+ const getSchema = {
1320
+ description: "Returns request information for GET requests",
1321
+ tags: ["HTTP Methods"],
1322
+ querystring: {
1323
+ type: "object",
1324
+ additionalProperties: true,
1325
+ properties: {},
1326
+ description: "Query parameters sent in the request"
1327
+ },
1328
+ headers: {
1329
+ type: "object",
1330
+ additionalProperties: true,
1331
+ properties: {},
1332
+ description: "Headers sent in the request"
1333
+ },
1334
+ response: { 200: {
1335
+ type: "object",
1336
+ properties: {
1337
+ method: { type: "string" },
1338
+ headers: {
1339
+ type: "object",
1340
+ additionalProperties: { type: "string" }
1341
+ },
1342
+ queryParams: {
1343
+ type: "object",
1344
+ additionalProperties: { type: "string" }
1345
+ }
1346
+ },
1347
+ required: [
1348
+ "method",
1349
+ "headers",
1350
+ "queryParams"
1351
+ ]
1352
+ } }
1353
+ };
1354
+ const getRoute = (fastify) => {
1355
+ fastify.get("/get", { schema: getSchema }, async (request, _reply) => ({
1356
+ method: "GET",
1357
+ headers: request.headers,
1358
+ queryParams: request.query
1359
+ }));
1360
+ };
1361
+ //#endregion
1362
+ //#region src/routes/http-methods/patch.ts
1363
+ const patchSchema = {
1364
+ description: "Handles a PATCH request and returns request information",
1365
+ tags: ["HTTP Methods"],
1366
+ body: {
1367
+ type: "object",
1368
+ additionalProperties: true,
1369
+ description: "The body of the PATCH request"
1370
+ },
1371
+ response: { 200: {
1372
+ type: "object",
1373
+ properties: {
1374
+ method: { type: "string" },
1375
+ headers: {
1376
+ type: "object",
1377
+ additionalProperties: { type: "string" }
1378
+ },
1379
+ body: {
1380
+ type: "object",
1381
+ additionalProperties: true
1382
+ }
1383
+ },
1384
+ required: [
1385
+ "method",
1386
+ "headers",
1387
+ "body"
1388
+ ]
1389
+ } }
1390
+ };
1391
+ const patchRoute = (fastify) => {
1392
+ fastify.patch("/patch", { schema: patchSchema }, async (request) => ({
1393
+ method: "PATCH",
1394
+ headers: request.headers,
1395
+ body: request.body
1396
+ }));
1397
+ };
1398
+ //#endregion
1399
+ //#region src/routes/http-methods/post.ts
1400
+ const postSchema = {
1401
+ description: "Handles a POST request and returns request information",
1402
+ tags: ["HTTP Methods"],
1403
+ body: {
1404
+ type: "object",
1405
+ additionalProperties: true,
1406
+ description: "The body of the POST request"
1407
+ },
1408
+ response: { 200: {
1409
+ type: "object",
1410
+ properties: {
1411
+ method: { type: "string" },
1412
+ headers: {
1413
+ type: "object",
1414
+ additionalProperties: { type: "string" }
1415
+ },
1416
+ body: {
1417
+ type: "object",
1418
+ additionalProperties: true
1419
+ }
1420
+ },
1421
+ required: [
1422
+ "method",
1423
+ "headers",
1424
+ "body"
1425
+ ]
1426
+ } }
1427
+ };
1428
+ const postRoute = (fastify) => {
1429
+ fastify.post("/post", { schema: postSchema }, async (request) => ({
1430
+ method: "POST",
1431
+ headers: request.headers,
1432
+ body: request.body
1433
+ }));
1434
+ };
1435
+ //#endregion
1436
+ //#region src/routes/http-methods/put.ts
1437
+ const putSchema = {
1438
+ description: "Handles a PUT request and returns request information",
1439
+ tags: ["HTTP Methods"],
1440
+ body: {
1441
+ type: "object",
1442
+ additionalProperties: true,
1443
+ description: "The body of the PUT request"
1444
+ },
1445
+ response: { 200: {
1446
+ type: "object",
1447
+ properties: {
1448
+ method: { type: "string" },
1449
+ headers: {
1450
+ type: "object",
1451
+ additionalProperties: { type: "string" }
1452
+ },
1453
+ body: {
1454
+ type: "object",
1455
+ additionalProperties: true
1456
+ }
1457
+ },
1458
+ required: [
1459
+ "method",
1460
+ "headers",
1461
+ "body"
1462
+ ]
1463
+ } }
1464
+ };
1465
+ const putRoute = (fastify) => {
1466
+ fastify.put("/put", { schema: putSchema }, async (request) => ({
1467
+ method: "PUT",
1468
+ headers: request.headers,
1469
+ body: request.body
1470
+ }));
1471
+ };
1472
+ //#endregion
1473
+ //#region src/routes/images/index.ts
1474
+ const imageSchema = {
1475
+ description: "Returns an image based on Accept header (supports jpeg, png, svg, webp)",
1476
+ tags: ["Images"],
1477
+ headers: {
1478
+ type: "object",
1479
+ properties: { accept: {
1480
+ type: "string",
1481
+ description: "Accept header for content negotiation"
1482
+ } }
1483
+ },
1484
+ response: { 200: {
1485
+ description: "Image file",
1486
+ type: "string",
1487
+ format: "binary"
1488
+ } }
1489
+ };
1490
+ const jpegSchema = {
1491
+ description: "Returns a JPEG image",
1492
+ tags: ["Images"],
1493
+ response: { 200: {
1494
+ description: "JPEG image",
1495
+ type: "string",
1496
+ format: "binary"
1497
+ } }
1498
+ };
1499
+ const pngSchema = {
1500
+ description: "Returns a PNG image",
1501
+ tags: ["Images"],
1502
+ response: { 200: {
1503
+ description: "PNG image",
1504
+ type: "string",
1505
+ format: "binary"
1506
+ } }
1507
+ };
1508
+ const svgSchema = {
1509
+ description: "Returns an SVG image",
1510
+ tags: ["Images"],
1511
+ response: { 200: {
1512
+ description: "SVG image",
1513
+ type: "string",
1514
+ format: "binary"
1515
+ } }
1516
+ };
1517
+ const webpSchema = {
1518
+ description: "Returns a WEBP image",
1519
+ tags: ["Images"],
1520
+ response: { 200: {
1521
+ description: "WEBP image",
1522
+ type: "string",
1523
+ format: "binary"
1524
+ } }
1525
+ };
1526
+ const imageRoutes = (fastify) => {
1527
+ const publicPath = join(process.cwd(), "public");
1528
+ fastify.get("/image", { schema: imageSchema }, async (request, reply) => {
1529
+ const accept = request.headers.accept || "";
1530
+ let file = "logo.png";
1531
+ let contentType = "image/png";
1532
+ if (accept.includes("image/jpeg") || accept.includes("image/jpg")) {
1533
+ file = "logo.jpg";
1534
+ contentType = "image/jpeg";
1535
+ } else if (accept.includes("image/svg+xml") || accept.includes("image/svg")) {
1536
+ file = "logo.svg";
1537
+ contentType = "image/svg+xml";
1538
+ } else if (accept.includes("image/webp")) {
1539
+ file = "logo.webp";
1540
+ contentType = "image/webp";
1541
+ } else if (accept.includes("image/png")) {
1542
+ file = "logo.png";
1543
+ contentType = "image/png";
1544
+ }
1545
+ try {
1546
+ const imageBuffer = readFileSync(join(publicPath, file));
1547
+ reply.type(contentType);
1548
+ return imageBuffer;
1549
+ } catch (error) {
1550
+ /* v8 ignore next -- @preserve */
1551
+ reply.code(500);
1552
+ /* v8 ignore next -- @preserve */
1553
+ return { error: `${error}` };
1554
+ }
1555
+ });
1556
+ fastify.get("/image/jpeg", { schema: jpegSchema }, async (_request, reply) => {
1557
+ try {
1558
+ const imageBuffer = readFileSync(join(publicPath, "logo.jpg"));
1559
+ reply.type("image/jpeg");
1560
+ return imageBuffer;
1561
+ } catch (error) {
1562
+ /* v8 ignore next -- @preserve */
1563
+ reply.code(500);
1564
+ /* v8 ignore next -- @preserve */
1565
+ return { error: `${error}` };
1566
+ }
1567
+ });
1568
+ fastify.get("/image/png", { schema: pngSchema }, async (_request, reply) => {
1569
+ try {
1570
+ const imageBuffer = readFileSync(join(publicPath, "logo.png"));
1571
+ reply.type("image/png");
1572
+ return imageBuffer;
1573
+ } catch (error) {
1574
+ /* v8 ignore next -- @preserve */
1575
+ reply.code(500);
1576
+ /* v8 ignore next -- @preserve */
1577
+ return { error: `${error}` };
1578
+ }
1579
+ });
1580
+ fastify.get("/image/svg", { schema: svgSchema }, async (_request, reply) => {
1581
+ try {
1582
+ const imageBuffer = readFileSync(join(publicPath, "logo.svg"));
1583
+ reply.type("image/svg+xml");
1584
+ return imageBuffer;
1585
+ } catch (error) {
1586
+ /* v8 ignore next -- @preserve */
1587
+ reply.code(500);
1588
+ /* v8 ignore next -- @preserve */
1589
+ return { error: `${error}` };
1590
+ }
1591
+ });
1592
+ fastify.get("/image/webp", { schema: webpSchema }, async (_request, reply) => {
1593
+ try {
1594
+ const imageBuffer = readFileSync(join(publicPath, "logo.webp"));
1595
+ reply.type("image/webp");
1596
+ return imageBuffer;
1597
+ } catch (error) {
1598
+ /* v8 ignore next -- @preserve */
1599
+ reply.code(500);
1600
+ /* v8 ignore next -- @preserve */
1601
+ return { error: `${error}` };
1602
+ }
1603
+ });
1604
+ };
1605
+ //#endregion
1606
+ //#region src/routes/index.ts
1607
+ const indexRoute = (fastify) => {
1608
+ fastify.get("/", { schema: { hide: true } }, async (_request, reply) => {
1609
+ await reply.type("text/html").send(`
1610
+ <!doctype html>
1611
+ <html>
1612
+ <head>
1613
+ <title>{ mockhttp }</title>
1614
+ <meta charset="utf-8" />
1615
+ <meta
1616
+ name="viewport"
1617
+ content="width=device-width, initial-scale=1" />
1618
+ <meta name="description" content="MockHttp - A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
1619
+ <meta name="keywords" content="mock, http, httpbin, testing, api, fastify, docker, typescript, rest, mock-server, api-testing, http-testing, httpbin-alternative" />
1620
+ <meta property="og:title" content="MockHttp - HTTP Mock Server" />
1621
+ <meta property="og:description" content="A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
1622
+ <meta property="og:type" content="website" />
1623
+ <meta property="og:url" content="https://mockhttp.org" />
1624
+ <meta property="og:image" content="https://mockhttp.org/logo.png" />
1625
+ <meta property="og:site_name" content="MockHttp" />
1626
+ <meta name="twitter:card" content="summary" />
1627
+ <meta name="twitter:title" content="MockHttp - HTTP Mock Server" />
1628
+ <meta name="twitter:description" content="A powerful HTTP mock server and httpbin replacement for API testing." />
1629
+ <meta name="twitter:image" content="https://mockhttp.org/logo.png" />
1630
+ <style>
1631
+ :root {
1632
+ --scalar-custom-header-height: 70px;
1633
+ }
1634
+ .custom-header {
1635
+ height: var(--scalar-custom-header-height);
1636
+ background-color: var(--scalar-background-1);
1637
+ box-shadow: inset 0 -1px 0 var(--scalar-border-color);
1638
+ color: var(--scalar-color-1);
1639
+ font-size: var(--scalar-font-size-2);
1640
+ padding: 0 18px;
1641
+ position: sticky;
1642
+ justify-content: space-between;
1643
+ top: 0;
1644
+ z-index: 100;
1645
+ }
1646
+ .custom-header,
1647
+ .custom-header nav {
1648
+ display: flex;
1649
+ align-items: center;
1650
+ gap: 18px;
1651
+ }
1652
+
1653
+ .custom-header img {
1654
+ height: var(--scalar-custom-header-height);
1655
+ object-fit: contain;
1656
+ }
1657
+
1658
+ .custom-header a:hover {
1659
+ color: var(--scalar-color-2);
1660
+ }
1661
+
1662
+ .github-corner {
1663
+ fill: var(--scalar-border-color) !important;
1664
+ }
1665
+
1666
+ </style>
1667
+ </head>
1668
+ <body>
1669
+ <header class="custom-header scalar-app">
1670
+ <a href="/"><img src="/logo.svg" alt="{ mockhttp }" /></a>
1671
+ <a href="https://github.com/jaredwray/mockhttp" class="github-corner" aria-label="View source on GitHub">
1672
+ <svg width="70" height="70" viewBox="0 0 250 250" style="color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
1673
+ <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
1674
+ <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
1675
+ <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
1676
+ </header>
1677
+
1678
+ <script
1679
+ id="api-reference"
1680
+ data-url="/docs/json"><\/script>
1681
+
1682
+ <script src="/scalar/browser/standalone.js"><\/script>
1683
+ </body>
1684
+ </html>
1685
+ `);
1686
+ });
1687
+ };
1688
+ //#endregion
1689
+ //#region src/routes/redirects/absolute-redirect.ts
1690
+ const absoluteRedirectSchema = {
1691
+ tags: ["Redirects"],
1692
+ description: "Redirects the request to the target URL using an absolute URL",
1693
+ params: {
1694
+ type: "object",
1695
+ properties: { value: {
1696
+ type: "integer",
1697
+ minimum: 1
1698
+ } },
1699
+ required: ["value"]
1700
+ },
1701
+ headers: {
1702
+ type: "object",
1703
+ properties: { host: { type: "string" } },
1704
+ required: ["host"]
1705
+ },
1706
+ response: { 302: { type: "string" } }
1707
+ };
1708
+ const absoluteRedirectRoute = (fastify) => {
1709
+ fastify.get("/absolute-redirect/:value", { schema: absoluteRedirectSchema }, async (request, reply) => {
1710
+ const { value } = request.params;
1711
+ const { host } = request.headers;
1712
+ const { protocol } = request;
1713
+ let url = `${protocol}://${host}/get`;
1714
+ let html = `
1715
+ <title>Redirecting...</title>
1716
+ <h1>Redirecting...</h1>
1717
+ <p>You should be redirected automatically to target URL: <a href="${url}">${url}</a>. If not click the link.</p>
1718
+ `;
1719
+ if (value > 1) {
1720
+ url = `${protocol}://${host}/absolute-redirect/${value - 1}`;
1721
+ html = `
1722
+ <title>Redirecting...</title>
1723
+ <h1>Redirecting...</h1>
1724
+ <p>You should be redirected automatically to target URL: <a href="${url}">${url}</a>. If not click the link.</p>
1725
+ `;
1726
+ }
1727
+ await reply.header("Location", url).status(302).type("text/html").send(html);
1728
+ });
1729
+ };
1730
+ //#endregion
1731
+ //#region src/routes/redirects/redirect-to.ts
1732
+ const redirectToSchema = {
1733
+ tags: ["Redirects"],
1734
+ description: "Redirects the request to the target URL",
1735
+ querystring: {
1736
+ type: "object",
1737
+ properties: {
1738
+ url: {
1739
+ type: "string",
1740
+ format: "uri",
1741
+ description: "The URL to redirect to"
1742
+ },
1743
+ status_code: {
1744
+ type: "string",
1745
+ pattern: "^[1-5][0-9][0-9]$",
1746
+ description: "The HTTP status code to use for redirection"
1747
+ }
1748
+ },
1749
+ required: ["url"],
1750
+ additionalProperties: false
1751
+ }
1752
+ };
1753
+ const redirectToRoute = (fastify) => {
1754
+ const handler = async (request, reply) => {
1755
+ const { url, status_code } = request.query;
1756
+ const statusCode = status_code ? Number(status_code) : 302;
1757
+ await reply.redirect(url, statusCode);
1758
+ };
1759
+ fastify.get("/redirect-to", { schema: redirectToSchema }, handler);
1760
+ fastify.post("/redirect-to", { schema: redirectToSchema }, handler);
1761
+ fastify.put("/redirect-to", { schema: redirectToSchema }, handler);
1762
+ fastify.patch("/redirect-to", { schema: redirectToSchema }, handler);
1763
+ fastify.delete("/redirect-to", { schema: redirectToSchema }, handler);
1764
+ };
1765
+ //#endregion
1766
+ //#region src/routes/redirects/relative-redirect.ts
1767
+ const relativeRedirectSchema = {
1768
+ tags: ["Redirects"],
1769
+ description: "Redirects the request to the target URL using an relative URL",
1770
+ params: {
1771
+ type: "object",
1772
+ properties: { value: {
1773
+ type: "integer",
1774
+ minimum: 1
1775
+ } },
1776
+ required: ["value"]
1777
+ },
1778
+ headers: {
1779
+ type: "object",
1780
+ properties: { host: { type: "string" } },
1781
+ required: ["host"]
1782
+ },
1783
+ response: { 302: { type: "string" } }
1784
+ };
1785
+ const relativeRedirectRoute = (fastify) => {
1786
+ fastify.get("/relative-redirect/:value", { schema: relativeRedirectSchema }, async (request, reply) => {
1787
+ const { value } = request.params;
1788
+ let url = "/get";
1789
+ let html = `
1790
+ <title>Redirecting...</title>
1791
+ <h1>Redirecting...</h1>
1792
+ <p>You should be redirected automatically to target URL: <a href="${url}">${url}</a>. If not click the link.</p>
1793
+ `;
1794
+ if (value > 1) {
1795
+ url = `/relative-redirect/${value - 1}`;
1796
+ html = `
1797
+ <title>Redirecting...</title>
1798
+ <h1>Redirecting...</h1>
1799
+ <p>You should be redirected automatically to target URL: <a href="${url}">${url}</a>. If not click the link.</p>
1800
+ `;
1801
+ }
1802
+ await reply.header("Location", url).status(302).type("text/html").send(html);
1803
+ });
1804
+ };
1805
+ //#endregion
1806
+ //#region src/routes/request-inspection/headers.ts
1807
+ const headersSchema = {
1808
+ description: "Returns all headers of the client request",
1809
+ tags: ["Request Inspection"],
1810
+ response: { 200: {
1811
+ type: "object",
1812
+ properties: { headers: {
1813
+ type: "object",
1814
+ additionalProperties: { type: "string" },
1815
+ description: "All headers of the client request"
1816
+ } },
1817
+ required: ["headers"]
1818
+ } }
1819
+ };
1820
+ const headersRoute = (fastify) => {
1821
+ fastify.get("/headers", { schema: headersSchema }, async (request) => {
1822
+ const { headers } = request;
1823
+ return { headers };
1824
+ });
1825
+ };
1826
+ //#endregion
1827
+ //#region src/routes/request-inspection/ip.ts
1828
+ const ipSchema = {
1829
+ description: "Returns the IP address of the client",
1830
+ tags: ["Request Inspection"],
1831
+ response: { 200: {
1832
+ type: "object",
1833
+ properties: { ip: {
1834
+ type: "string",
1835
+ description: "The IP address of the client"
1836
+ } },
1837
+ required: ["ip"]
1838
+ } }
1839
+ };
1840
+ const ipRoute = (fastify) => {
1841
+ fastify.get("/ip", { schema: ipSchema }, async (request) => {
1842
+ const cloudflareIp = request.headers["cf-connecting-ip"];
1843
+ const forwardedIp = request.headers["x-forwarded-for"];
1844
+ const remoteIp = request.ip;
1845
+ return { ip: cloudflareIp ?? (typeof forwardedIp === "string" ? forwardedIp.split(",")[0].trim() : remoteIp) };
1846
+ });
1847
+ };
1848
+ //#endregion
1849
+ //#region src/routes/request-inspection/user-agent.ts
1850
+ const userAgentSchema = {
1851
+ description: "Returns the User-Agent header of the client request",
1852
+ tags: ["Request Inspection"],
1853
+ response: { 200: {
1854
+ type: "object",
1855
+ properties: { userAgent: {
1856
+ type: "string",
1857
+ description: "The User-Agent header of the client request"
1858
+ } },
1859
+ required: ["userAgent"]
1860
+ } }
1861
+ };
1862
+ const userAgentRoute = (fastify) => {
1863
+ fastify.get("/user-agent", { schema: userAgentSchema }, async (request) => {
1864
+ return { userAgent: request.headers["user-agent"] };
1865
+ });
1866
+ };
1867
+ //#endregion
1868
+ //#region src/routes/response-formats/index.ts
1869
+ const plainSchema = {
1870
+ description: "Returns random plain text content",
1871
+ tags: ["Response Formats"],
1872
+ response: { 200: {
1873
+ type: "string",
1874
+ description: "Random plain text content"
1875
+ } }
1876
+ };
1877
+ const textSchema = {
1878
+ description: "Returns random text content",
1879
+ tags: ["Response Formats"],
1880
+ response: { 200: {
1881
+ type: "string",
1882
+ description: "Random text content"
1883
+ } }
1884
+ };
1885
+ const htmlSchema = {
1886
+ description: "Returns random HTML content",
1887
+ tags: ["Response Formats"],
1888
+ response: { 200: {
1889
+ type: "string",
1890
+ description: "Random HTML content"
1891
+ } }
1892
+ };
1893
+ const xmlSchema = {
1894
+ description: "Returns random XML content",
1895
+ tags: ["Response Formats"],
1896
+ response: { 200: {
1897
+ type: "string",
1898
+ description: "Random XML content"
1899
+ } }
1900
+ };
1901
+ const jsonSchema = {
1902
+ description: "Returns random JSON content",
1903
+ tags: ["Response Formats"],
1904
+ response: { 200: {
1905
+ type: "object",
1906
+ description: "Random JSON content",
1907
+ additionalProperties: true
1908
+ } }
1909
+ };
1910
+ const denySchema = {
1911
+ description: "Returns page denied by robots.txt rules",
1912
+ tags: ["Response Formats"],
1913
+ response: { 403: {
1914
+ type: "string",
1915
+ description: "Page denied by robots.txt rules"
1916
+ } }
1917
+ };
1918
+ const gzipSchema = {
1919
+ description: "Returns GZip-encoded data",
1920
+ tags: ["Response Formats"],
1921
+ response: { 200: {
1922
+ type: "string",
1923
+ description: "GZip-encoded content"
1924
+ } }
1925
+ };
1926
+ const deflateSchema = {
1927
+ description: "Returns Deflate-encoded data",
1928
+ tags: ["Response Formats"],
1929
+ response: { 200: {
1930
+ type: "string",
1931
+ description: "Deflate-encoded content"
1932
+ } }
1933
+ };
1934
+ const brotliSchema = {
1935
+ description: "Returns Brotli-encoded data",
1936
+ tags: ["Response Formats"],
1937
+ response: { 200: {
1938
+ type: "string",
1939
+ description: "Brotli-encoded content"
1940
+ } }
1941
+ };
1942
+ const randomTexts = [
1943
+ "The quick brown fox jumps over the lazy dog.",
1944
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
1945
+ "To be or not to be, that is the question.",
1946
+ "All work and no play makes Jack a dull boy.",
1947
+ "The only way to do great work is to love what you do.",
1948
+ "In the beginning was the Word, and the Word was with God.",
1949
+ "Once upon a time in a galaxy far, far away...",
1950
+ "It was the best of times, it was the worst of times.",
1951
+ "The journey of a thousand miles begins with a single step.",
1952
+ "Knowledge is power. France is bacon."
1953
+ ];
1954
+ const randomHtmlContent = [
1955
+ `<!DOCTYPE html>
1956
+ <html lang="en">
1957
+ <head>
1958
+ <title>Sample Page</title>
1959
+ </head>
1960
+ <body>
1961
+ <h1>Welcome to MockHTTP</h1>
1962
+ <p>This is a sample HTML page with random content.</p>
1963
+ </body>
1964
+ </html>`,
1965
+ `<!DOCTYPE html>
1966
+ <html>
1967
+ <head>
1968
+ <meta charset="UTF-8">
1969
+ <title>Test Document</title>
1970
+ </head>
1971
+ <body>
1972
+ <header>
1973
+ <h1>Test Page</h1>
1974
+ <nav>
1975
+ <ul>
1976
+ <li><a href="#">Home</a></li>
1977
+ <li><a href="#">About</a></li>
1978
+ <li><a href="#">Contact</a></li>
1979
+ </ul>
1980
+ </nav>
1981
+ </header>
1982
+ <main>
1983
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
1984
+ </main>
1985
+ </body>
1986
+ </html>`,
1987
+ `<!DOCTYPE html>
1988
+ <html lang="en">
1989
+ <head>
1990
+ <title>Article Page</title>
1991
+ <style>
1992
+ body { font-family: Arial, sans-serif; }
1993
+ article { max-width: 800px; margin: 0 auto; }
1994
+ </style>
1995
+ </head>
1996
+ <body>
1997
+ <article>
1998
+ <h1>The Journey Begins</h1>
1999
+ <p>Once upon a time in a digital world...</p>
2000
+ <blockquote>
2001
+ "The only way to do great work is to love what you do."
2002
+ </blockquote>
2003
+ </article>
2004
+ </body>
2005
+ </html>`,
2006
+ `<!DOCTYPE html>
2007
+ <html>
2008
+ <head>
2009
+ <title>Form Example</title>
2010
+ </head>
2011
+ <body>
2012
+ <h2>Contact Form</h2>
2013
+ <form action="/submit" method="post">
2014
+ <label for="name">Name:</label>
2015
+ <input type="text" id="name" name="name" required>
2016
+ <br>
2017
+ <label for="email">Email:</label>
2018
+ <input type="email" id="email" name="email" required>
2019
+ <br>
2020
+ <button type="submit">Submit</button>
2021
+ </form>
2022
+ </body>
2023
+ </html>`,
2024
+ `<!DOCTYPE html>
2025
+ <html lang="en">
2026
+ <head>
2027
+ <meta charset="UTF-8">
2028
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2029
+ <title>Responsive Page</title>
2030
+ </head>
2031
+ <body>
2032
+ <div class="container">
2033
+ <h1>Responsive Design</h1>
2034
+ <p>This page adapts to different screen sizes.</p>
2035
+ <img src="placeholder.jpg" alt="Placeholder" style="max-width: 100%; height: auto;">
2036
+ </div>
2037
+ </body>
2038
+ </html>`
2039
+ ];
2040
+ const randomXmlContent = [
2041
+ `<?xml version="1.0" encoding="UTF-8"?>
2042
+ <root>
2043
+ <message>Hello from MockHTTP</message>
2044
+ <timestamp>${(/* @__PURE__ */ new Date()).toISOString()}</timestamp>
2045
+ <status>success</status>
2046
+ </root>`,
2047
+ `<?xml version="1.0" encoding="UTF-8"?>
2048
+ <book>
2049
+ <title>The Great Gatsby</title>
2050
+ <author>F. Scott Fitzgerald</author>
2051
+ <year>1925</year>
2052
+ <publisher>Charles Scribner's Sons</publisher>
2053
+ <isbn>978-0-7432-7356-5</isbn>
2054
+ </book>`,
2055
+ `<?xml version="1.0" encoding="UTF-8"?>
2056
+ <users>
2057
+ <user id="1">
2058
+ <name>John Doe</name>
2059
+ <email>john@example.com</email>
2060
+ <role>admin</role>
2061
+ </user>
2062
+ <user id="2">
2063
+ <name>Jane Smith</name>
2064
+ <email>jane@example.com</email>
2065
+ <role>user</role>
2066
+ </user>
2067
+ </users>`,
2068
+ `<?xml version="1.0" encoding="UTF-8"?>
2069
+ <product>
2070
+ <id>12345</id>
2071
+ <name>Wireless Mouse</name>
2072
+ <description>Ergonomic wireless mouse with 3-year battery life</description>
2073
+ <price currency="USD">29.99</price>
2074
+ <stock>150</stock>
2075
+ <category>Electronics</category>
2076
+ </product>`,
2077
+ `<?xml version="1.0" encoding="UTF-8"?>
2078
+ <rss version="2.0">
2079
+ <channel>
2080
+ <title>Mock RSS Feed</title>
2081
+ <link>http://example.com/rss</link>
2082
+ <description>Sample RSS feed from MockHTTP</description>
2083
+ <item>
2084
+ <title>First Article</title>
2085
+ <link>http://example.com/article1</link>
2086
+ <description>This is the first article in our feed</description>
2087
+ <pubDate>${(/* @__PURE__ */ new Date()).toUTCString()}</pubDate>
2088
+ </item>
2089
+ </channel>
2090
+ </rss>`
2091
+ ];
2092
+ const randomJsonContent = [
2093
+ () => ({
2094
+ message: "Hello from MockHTTP",
2095
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2096
+ status: "success",
2097
+ code: 200
2098
+ }),
2099
+ () => ({
2100
+ user: {
2101
+ id: 1,
2102
+ name: "John Doe",
2103
+ email: "john@example.com",
2104
+ role: "admin",
2105
+ createdAt: "2024-01-15T10:30:00Z"
2106
+ },
2107
+ permissions: [
2108
+ "read",
2109
+ "write",
2110
+ "delete"
2111
+ ]
2112
+ }),
2113
+ () => ({
2114
+ products: [{
2115
+ id: 101,
2116
+ name: "Laptop",
2117
+ price: 999.99,
2118
+ stock: 50
2119
+ }, {
2120
+ id: 102,
2121
+ name: "Mouse",
2122
+ price: 29.99,
2123
+ stock: 200
2124
+ }],
2125
+ total: 2,
2126
+ page: 1
2127
+ }),
2128
+ () => ({ config: {
2129
+ theme: "dark",
2130
+ language: "en",
2131
+ notifications: {
2132
+ email: true,
2133
+ push: false,
2134
+ sms: false
2135
+ },
2136
+ privacy: {
2137
+ profile: "public",
2138
+ activity: "friends"
2139
+ }
2140
+ } }),
2141
+ () => ({ metrics: {
2142
+ cpu: 45.2,
2143
+ memory: 78.5,
2144
+ disk: 62.3,
2145
+ network: {
2146
+ inbound: "1.2MB/s",
2147
+ outbound: "0.8MB/s"
2148
+ },
2149
+ uptime: "15 days",
2150
+ services: [
2151
+ {
2152
+ name: "api",
2153
+ status: "healthy"
2154
+ },
2155
+ {
2156
+ name: "database",
2157
+ status: "healthy"
2158
+ },
2159
+ {
2160
+ name: "cache",
2161
+ status: "warning"
2162
+ }
2163
+ ]
2164
+ } })
2165
+ ];
2166
+ const responseFormatRoutes = (fastify) => {
2167
+ const plainHandler = async (_request, reply) => {
2168
+ const text = randomTexts[Math.floor(Math.random() * randomTexts.length)];
2169
+ reply.type("text/plain");
2170
+ return text;
2171
+ };
2172
+ fastify.get("/plain", { schema: plainSchema }, plainHandler);
2173
+ fastify.post("/plain", { schema: plainSchema }, plainHandler);
2174
+ fastify.put("/plain", { schema: plainSchema }, plainHandler);
2175
+ fastify.patch("/plain", { schema: plainSchema }, plainHandler);
2176
+ fastify.delete("/plain", { schema: plainSchema }, plainHandler);
2177
+ const textHandler = async (_request, reply) => {
2178
+ const text = randomTexts[Math.floor(Math.random() * randomTexts.length)];
2179
+ reply.type("text/plain");
2180
+ return text;
2181
+ };
2182
+ fastify.get("/text", { schema: textSchema }, textHandler);
2183
+ fastify.post("/text", { schema: textSchema }, textHandler);
2184
+ fastify.put("/text", { schema: textSchema }, textHandler);
2185
+ fastify.patch("/text", { schema: textSchema }, textHandler);
2186
+ fastify.delete("/text", { schema: textSchema }, textHandler);
2187
+ const htmlHandler = async (_request, reply) => {
2188
+ const html = randomHtmlContent[Math.floor(Math.random() * randomHtmlContent.length)];
2189
+ reply.type("text/html");
2190
+ return html;
2191
+ };
2192
+ fastify.get("/html", { schema: htmlSchema }, htmlHandler);
2193
+ fastify.post("/html", { schema: htmlSchema }, htmlHandler);
2194
+ fastify.put("/html", { schema: htmlSchema }, htmlHandler);
2195
+ fastify.patch("/html", { schema: htmlSchema }, htmlHandler);
2196
+ fastify.delete("/html", { schema: htmlSchema }, htmlHandler);
2197
+ const xmlHandler = async (_request, reply) => {
2198
+ const xml = randomXmlContent[Math.floor(Math.random() * randomXmlContent.length)];
2199
+ reply.type("application/xml");
2200
+ return xml;
2201
+ };
2202
+ fastify.get("/xml", { schema: xmlSchema }, xmlHandler);
2203
+ fastify.post("/xml", { schema: xmlSchema }, xmlHandler);
2204
+ fastify.put("/xml", { schema: xmlSchema }, xmlHandler);
2205
+ fastify.patch("/xml", { schema: xmlSchema }, xmlHandler);
2206
+ fastify.delete("/xml", { schema: xmlSchema }, xmlHandler);
2207
+ const jsonHandler = async (_request, reply) => {
2208
+ const jsonGenerator = randomJsonContent[Math.floor(Math.random() * randomJsonContent.length)];
2209
+ const json = jsonGenerator();
2210
+ reply.type("application/json");
2211
+ return json;
2212
+ };
2213
+ fastify.get("/json", { schema: jsonSchema }, jsonHandler);
2214
+ fastify.post("/json", { schema: jsonSchema }, jsonHandler);
2215
+ fastify.put("/json", { schema: jsonSchema }, jsonHandler);
2216
+ fastify.patch("/json", { schema: jsonSchema }, jsonHandler);
2217
+ fastify.delete("/json", { schema: jsonSchema }, jsonHandler);
2218
+ const denyHandler = async (_request, reply) => {
2219
+ const denyMessages = [
2220
+ `<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
2221
+ <html><head>
2222
+ <title>403 Forbidden</title>
2223
+ </head><body>
2224
+ <h1>Forbidden</h1>
2225
+ <p>Access denied by robots.txt rules.</p>
2226
+ <p>The requested resource is disallowed by robots.txt.</p>
2227
+ </body></html>`,
2228
+ `<!DOCTYPE html>
2229
+ <html lang="en">
2230
+ <head>
2231
+ <meta charset="UTF-8">
2232
+ <title>403 - Access Denied</title>
2233
+ </head>
2234
+ <body>
2235
+ <h1>403 Forbidden</h1>
2236
+ <p>This page is blocked by robots.txt rules.</p>
2237
+ <p>User-agent: *<br>Disallow: ${_request.url}</p>
2238
+ </body>
2239
+ </html>`,
2240
+ `<!DOCTYPE html>
2241
+ <html>
2242
+ <head>
2243
+ <title>403 - Access Denied</title>
2244
+ <style>body { font-family: Arial, sans-serif; margin: 40px; }</style>
2245
+ </head>
2246
+ <body>
2247
+ <h2>403 - Access Denied by robots.txt</h2>
2248
+ <p>The robots.txt file prevents access to this resource.</p>
2249
+ <hr>
2250
+ <small>MockHTTP Server</small>
2251
+ </body>
2252
+ </html>`
2253
+ ];
2254
+ const html = denyMessages[Math.floor(Math.random() * denyMessages.length)];
2255
+ reply.code(403);
2256
+ reply.type("text/html");
2257
+ return html;
2258
+ };
2259
+ fastify.get("/deny", { schema: denySchema }, denyHandler);
2260
+ fastify.post("/deny", { schema: denySchema }, denyHandler);
2261
+ fastify.put("/deny", { schema: denySchema }, denyHandler);
2262
+ fastify.patch("/deny", { schema: denySchema }, denyHandler);
2263
+ fastify.delete("/deny", { schema: denySchema }, denyHandler);
2264
+ const gzipHandler = async (_request, reply) => {
2265
+ const sampleData = [
2266
+ {
2267
+ message: "This is GZip compressed data from MockHTTP",
2268
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2269
+ compression: "gzip",
2270
+ originalSize: 0,
2271
+ compressedSize: 0
2272
+ },
2273
+ {
2274
+ title: "Sample Document",
2275
+ content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(50),
2276
+ metadata: {
2277
+ author: "MockHTTP Server",
2278
+ format: "gzip",
2279
+ version: "1.0"
2280
+ }
2281
+ },
2282
+ {
2283
+ data: Array.from({ length: 100 }, (_, i) => ({
2284
+ id: i + 1,
2285
+ value: `Item ${i + 1}`,
2286
+ description: `This is item number ${i + 1} in the dataset`
2287
+ })),
2288
+ info: "Large dataset compressed with GZip"
2289
+ }
2290
+ ];
2291
+ const data = sampleData[Math.floor(Math.random() * sampleData.length)];
2292
+ const jsonString = JSON.stringify(data);
2293
+ if (data.originalSize !== void 0) data.originalSize = Buffer.byteLength(jsonString);
2294
+ const compressed = gzipSync(jsonString);
2295
+ if (data.compressedSize !== void 0) {
2296
+ const updatedData = {
2297
+ ...data,
2298
+ compressedSize: compressed.length
2299
+ };
2300
+ const updatedCompressed = gzipSync(JSON.stringify(updatedData));
2301
+ reply.header("Content-Encoding", "gzip");
2302
+ reply.header("Content-Type", "application/json");
2303
+ reply.type("application/octet-stream");
2304
+ return updatedCompressed;
2305
+ }
2306
+ reply.header("Content-Encoding", "gzip");
2307
+ reply.header("Content-Type", "application/json");
2308
+ reply.type("application/octet-stream");
2309
+ return compressed;
2310
+ };
2311
+ fastify.get("/gzip", { schema: gzipSchema }, gzipHandler);
2312
+ fastify.post("/gzip", { schema: gzipSchema }, gzipHandler);
2313
+ fastify.put("/gzip", { schema: gzipSchema }, gzipHandler);
2314
+ fastify.patch("/gzip", { schema: gzipSchema }, gzipHandler);
2315
+ fastify.delete("/gzip", { schema: gzipSchema }, gzipHandler);
2316
+ const deflateHandler = async (_request, reply) => {
2317
+ const sampleData = [
2318
+ {
2319
+ message: "This is Deflate compressed data from MockHTTP",
2320
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2321
+ compression: "deflate",
2322
+ originalSize: 0,
2323
+ compressedSize: 0
2324
+ },
2325
+ {
2326
+ title: "Deflate Compressed Document",
2327
+ content: "The deflate algorithm is used for compression. ".repeat(40),
2328
+ details: {
2329
+ algorithm: "DEFLATE",
2330
+ method: "zlib deflate",
2331
+ server: "MockHTTP"
2332
+ }
2333
+ },
2334
+ {
2335
+ items: Array.from({ length: 50 }, (_, i) => ({
2336
+ index: i,
2337
+ name: `Entry ${i}`,
2338
+ data: `This is test data for entry number ${i}`,
2339
+ timestamp: (/* @__PURE__ */ new Date(Date.now() - i * 1e6)).toISOString()
2340
+ })),
2341
+ metadata: {
2342
+ compression: "deflate",
2343
+ count: 50
2344
+ }
2345
+ }
2346
+ ];
2347
+ const data = sampleData[Math.floor(Math.random() * sampleData.length)];
2348
+ const jsonString = JSON.stringify(data);
2349
+ if (data.originalSize !== void 0) data.originalSize = Buffer.byteLength(jsonString);
2350
+ const compressed = deflateSync(jsonString);
2351
+ if (data.compressedSize !== void 0) {
2352
+ const updatedData = {
2353
+ ...data,
2354
+ compressedSize: compressed.length
2355
+ };
2356
+ const updatedCompressed = deflateSync(JSON.stringify(updatedData));
2357
+ reply.header("Content-Encoding", "deflate");
2358
+ reply.header("Content-Type", "application/json");
2359
+ reply.type("application/octet-stream");
2360
+ return updatedCompressed;
2361
+ }
2362
+ reply.header("Content-Encoding", "deflate");
2363
+ reply.header("Content-Type", "application/json");
2364
+ reply.type("application/octet-stream");
2365
+ return compressed;
2366
+ };
2367
+ fastify.get("/deflate", { schema: deflateSchema }, deflateHandler);
2368
+ fastify.post("/deflate", { schema: deflateSchema }, deflateHandler);
2369
+ fastify.put("/deflate", { schema: deflateSchema }, deflateHandler);
2370
+ fastify.patch("/deflate", { schema: deflateSchema }, deflateHandler);
2371
+ fastify.delete("/deflate", { schema: deflateSchema }, deflateHandler);
2372
+ const brotliHandler = async (_request, reply) => {
2373
+ const sampleData = [
2374
+ {
2375
+ message: "This is Brotli compressed data from MockHTTP",
2376
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2377
+ compression: "br",
2378
+ algorithm: "Brotli",
2379
+ originalSize: 0,
2380
+ compressedSize: 0
2381
+ },
2382
+ {
2383
+ title: "Brotli Compression Example",
2384
+ description: "Brotli is a generic-purpose lossless compression algorithm that compresses data using a combination of a modern variant of the LZ77 algorithm, Huffman coding and 2nd order context modeling.",
2385
+ content: "Brotli compression typically achieves 20-26% better compression ratios than gzip. ".repeat(10),
2386
+ metadata: {
2387
+ encoding: "br",
2388
+ quality: 11,
2389
+ server: "MockHTTP"
2390
+ }
2391
+ },
2392
+ { benchmark: {
2393
+ title: "Compression Benchmark Data",
2394
+ results: Array.from({ length: 75 }, (_, i) => ({
2395
+ id: `test-${i}`,
2396
+ name: `Benchmark Test ${i}`,
2397
+ value: Math.random() * 1e3,
2398
+ unit: "ms",
2399
+ timestamp: (/* @__PURE__ */ new Date(Date.now() - i * 6e4)).toISOString()
2400
+ })),
2401
+ summary: {
2402
+ compression: "brotli",
2403
+ totalTests: 75,
2404
+ encoding: "br"
2405
+ }
2406
+ } }
2407
+ ];
2408
+ const data = sampleData[Math.floor(Math.random() * sampleData.length)];
2409
+ const jsonString = JSON.stringify(data);
2410
+ if (data.originalSize !== void 0) data.originalSize = Buffer.byteLength(jsonString);
2411
+ const compressed = brotliCompressSync(jsonString);
2412
+ if (data.compressedSize !== void 0) {
2413
+ const updatedData = {
2414
+ ...data,
2415
+ compressedSize: compressed.length
2416
+ };
2417
+ const updatedCompressed = brotliCompressSync(JSON.stringify(updatedData));
2418
+ reply.header("Content-Encoding", "br");
2419
+ reply.header("Content-Type", "application/json");
2420
+ reply.type("application/octet-stream");
2421
+ return updatedCompressed;
2422
+ }
2423
+ reply.header("Content-Encoding", "br");
2424
+ reply.header("Content-Type", "application/json");
2425
+ reply.type("application/octet-stream");
2426
+ return compressed;
2427
+ };
2428
+ fastify.get("/brotli", { schema: brotliSchema }, brotliHandler);
2429
+ fastify.post("/brotli", { schema: brotliSchema }, brotliHandler);
2430
+ fastify.put("/brotli", { schema: brotliSchema }, brotliHandler);
2431
+ fastify.patch("/brotli", { schema: brotliSchema }, brotliHandler);
2432
+ fastify.delete("/brotli", { schema: brotliSchema }, brotliHandler);
2433
+ };
2434
+ //#endregion
2435
+ //#region src/routes/response-inspection/cache.ts
2436
+ const cacheGetSchema = {
2437
+ description: "Handles cache validation and retrieval.",
2438
+ tags: ["Response Inspection"],
2439
+ response: {
2440
+ 200: {
2441
+ type: "string",
2442
+ description: "The cached content or resource."
2443
+ },
2444
+ 304: {
2445
+ type: "null",
2446
+ description: "Indicates the resource has not been modified."
2447
+ }
2448
+ }
2449
+ };
2450
+ const cacheSetSchema = {
2451
+ description: "Sets a Cache-Control header for n seconds.",
2452
+ tags: ["Response Inspection"],
2453
+ params: {
2454
+ type: "object",
2455
+ properties: { value: {
2456
+ type: "integer",
2457
+ description: "Number of seconds to cache the resource."
2458
+ } },
2459
+ required: ["value"]
2460
+ },
2461
+ response: { 200: {
2462
+ type: "string",
2463
+ description: "Confirmation message for setting cache."
2464
+ } }
2465
+ };
2466
+ const cacheRoutes = (fastify) => {
2467
+ fastify.get("/cache", { schema: cacheGetSchema }, async (request, reply) => {
2468
+ const ifModifiedSince = request.headers["if-modified-since"];
2469
+ const ifNoneMatch = request.headers["if-none-match"];
2470
+ if (ifModifiedSince ?? ifNoneMatch) {
2471
+ await reply.status(304).send();
2472
+ return;
2473
+ }
2474
+ await reply.send("Cache content or resource response here.");
2475
+ });
2476
+ fastify.get("/cache/:value", { schema: cacheSetSchema }, async (request, reply) => {
2477
+ const { value } = request.params;
2478
+ await reply.header("Cache-Control", `max-age=${value}`).send(`Cache-Control set for ${value} seconds.`);
2479
+ });
2480
+ };
2481
+ //#endregion
2482
+ //#region src/routes/response-inspection/etag.ts
2483
+ const etagSchema = {
2484
+ description: "Handles ETag-based conditional requests.",
2485
+ tags: ["Response Inspection"],
2486
+ params: {
2487
+ type: "object",
2488
+ properties: { etag: {
2489
+ type: "string",
2490
+ description: "The ETag value for the resource."
2491
+ } },
2492
+ required: ["etag"]
2493
+ },
2494
+ response: {
2495
+ 200: {
2496
+ type: "string",
2497
+ description: "Resource content for matching ETag."
2498
+ },
2499
+ 304: {
2500
+ type: "null",
2501
+ description: "Indicates the resource has not been modified."
2502
+ },
2503
+ 412: {
2504
+ type: "null",
2505
+ description: "Indicates a precondition failed due to mismatched ETag."
2506
+ }
2507
+ }
2508
+ };
2509
+ const etagRoutes = (fastify) => {
2510
+ fastify.get("/etag/:etag", { schema: etagSchema }, async (request, reply) => {
2511
+ const { etag } = request.params;
2512
+ const ifNoneMatch = request.headers["if-none-match"];
2513
+ const ifMatch = request.headers["if-match"];
2514
+ if (ifNoneMatch === etag) {
2515
+ await reply.status(304).send();
2516
+ return;
2517
+ }
2518
+ if (ifMatch && ifMatch !== etag) {
2519
+ await reply.status(412).send();
2520
+ return;
2521
+ }
2522
+ await reply.header("ETag", etag).send("Resource content for matching ETag.");
2523
+ });
2524
+ };
2525
+ //#endregion
2526
+ //#region src/routes/response-inspection/response-headers.ts
2527
+ const responseHeadersSchema = {
2528
+ description: "Returns a set of response headers based on the query string.",
2529
+ tags: ["Response Inspection"],
2530
+ querystring: {
2531
+ type: "object",
2532
+ additionalProperties: true,
2533
+ description: "Freeform query string parameters to be added as response headers."
2534
+ },
2535
+ response: { 200: {
2536
+ type: "object",
2537
+ additionalProperties: true,
2538
+ description: "Response headers as per query string parameters."
2539
+ } }
2540
+ };
2541
+ const responseHeadersRoutes = (fastify) => {
2542
+ fastify.get("/response-headers", { schema: responseHeadersSchema }, async (request, reply) => {
2543
+ const queryParameters = request.query;
2544
+ for (const [key, value] of Object.entries(queryParameters)) reply.header(key, escape(value));
2545
+ const cleanedResponse = Object.fromEntries(Object.entries(queryParameters).map(([key, value]) => [key, escape(value)]));
2546
+ await reply.send(cleanedResponse);
2547
+ });
2548
+ fastify.post("/response-headers", { schema: responseHeadersSchema }, async (request, reply) => {
2549
+ const queryParameters = request.query ?? {};
2550
+ const body = request.body ?? {};
2551
+ for (const [key, value] of Object.entries(queryParameters)) reply.header(key, escape(value));
2552
+ for (const [key, value] of Object.entries(body)) reply.header(key, escape(value));
2553
+ const cleanedResponse = Object.fromEntries(Object.entries({
2554
+ ...queryParameters,
2555
+ ...body
2556
+ }).map(([key, value]) => [key, escape(value)]));
2557
+ await reply.send(cleanedResponse);
2558
+ });
2559
+ };
2560
+ //#endregion
2561
+ //#region src/routes/sitemap.ts
2562
+ const sitemapRoute = (fastify) => {
2563
+ fastify.get("/sitemap.xml", { schema: { hide: true } }, async (_request, reply) => {
2564
+ try {
2565
+ const host = _request.headers.host ?? "mockhttp.org";
2566
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
2567
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
2568
+ <url>
2569
+ <loc>${_request.headers["x-forwarded-proto"]?.includes("https") ? "https" : "http"}://${host}/</loc>
2570
+ <changefreq>monthly</changefreq>
2571
+ <priority>1.0</priority>
2572
+ </url>
2573
+ </urlset>`;
2574
+ await reply.type("text/xml").send(xml);
2575
+ } catch (error) {
2576
+ /* v8 ignore next -- @preserve */
2577
+ fastify.log.error(error);
2578
+ /* v8 ignore next -- @preserve */
2579
+ await reply.status(500).send({ error: "Internal Server Error" });
2580
+ }
2581
+ });
2582
+ };
2583
+ //#endregion
2584
+ //#region src/routes/status-codes/index.ts
2585
+ const statusCodes = [
2586
+ {
2587
+ code: 100,
2588
+ message: "Continue"
2589
+ },
2590
+ {
2591
+ code: 101,
2592
+ message: "Switching Protocols"
2593
+ },
2594
+ {
2595
+ code: 102,
2596
+ message: "Processing"
2597
+ },
2598
+ {
2599
+ code: 103,
2600
+ message: "Early Hints"
2601
+ },
2602
+ {
2603
+ code: 200,
2604
+ message: "OK"
2605
+ },
2606
+ {
2607
+ code: 201,
2608
+ message: "Created"
2609
+ },
2610
+ {
2611
+ code: 202,
2612
+ message: "Accepted"
2613
+ },
2614
+ {
2615
+ code: 203,
2616
+ message: "Non-Authoritative Information"
2617
+ },
2618
+ {
2619
+ code: 204,
2620
+ message: "No Content"
2621
+ },
2622
+ {
2623
+ code: 205,
2624
+ message: "Reset Content"
2625
+ },
2626
+ {
2627
+ code: 206,
2628
+ message: "Partial Content"
2629
+ },
2630
+ {
2631
+ code: 207,
2632
+ message: "Multi-Status"
2633
+ },
2634
+ {
2635
+ code: 208,
2636
+ message: "Already Reported"
2637
+ },
2638
+ {
2639
+ code: 226,
2640
+ message: "IM Used"
2641
+ },
2642
+ {
2643
+ code: 300,
2644
+ message: "Multiple Choices"
2645
+ },
2646
+ {
2647
+ code: 301,
2648
+ message: "Moved Permanently"
2649
+ },
2650
+ {
2651
+ code: 302,
2652
+ message: "Found"
2653
+ },
2654
+ {
2655
+ code: 303,
2656
+ message: "See Other"
2657
+ },
2658
+ {
2659
+ code: 304,
2660
+ message: "Not Modified"
2661
+ },
2662
+ {
2663
+ code: 305,
2664
+ message: "Use Proxy"
2665
+ },
2666
+ {
2667
+ code: 306,
2668
+ message: "Switch Proxy"
2669
+ },
2670
+ {
2671
+ code: 307,
2672
+ message: "Temporary Redirect"
2673
+ },
2674
+ {
2675
+ code: 308,
2676
+ message: "Permanent Redirect"
2677
+ },
2678
+ {
2679
+ code: 400,
2680
+ message: "Bad Request"
2681
+ },
2682
+ {
2683
+ code: 401,
2684
+ message: "Unauthorized"
2685
+ },
2686
+ {
2687
+ code: 402,
2688
+ message: "Payment Required"
2689
+ },
2690
+ {
2691
+ code: 403,
2692
+ message: "Forbidden"
2693
+ },
2694
+ {
2695
+ code: 404,
2696
+ message: "Not Found"
2697
+ },
2698
+ {
2699
+ code: 405,
2700
+ message: "Method Not Allowed"
2701
+ },
2702
+ {
2703
+ code: 406,
2704
+ message: "Not Acceptable"
2705
+ },
2706
+ {
2707
+ code: 407,
2708
+ message: "Proxy Authentication Required"
2709
+ },
2710
+ {
2711
+ code: 408,
2712
+ message: "Request Timeout"
2713
+ },
2714
+ {
2715
+ code: 409,
2716
+ message: "Conflict"
2717
+ },
2718
+ {
2719
+ code: 410,
2720
+ message: "Gone"
2721
+ },
2722
+ {
2723
+ code: 411,
2724
+ message: "Length Required"
2725
+ },
2726
+ {
2727
+ code: 412,
2728
+ message: "Precondition Failed"
2729
+ },
2730
+ {
2731
+ code: 413,
2732
+ message: "Payload Too Large"
2733
+ },
2734
+ {
2735
+ code: 414,
2736
+ message: "URI Too Long"
2737
+ },
2738
+ {
2739
+ code: 415,
2740
+ message: "Unsupported Media Type"
2741
+ },
2742
+ {
2743
+ code: 416,
2744
+ message: "Range Not Satisfiable"
2745
+ },
2746
+ {
2747
+ code: 417,
2748
+ message: "Expectation Failed"
2749
+ },
2750
+ {
2751
+ code: 418,
2752
+ message: "I'm a teapot"
2753
+ },
2754
+ {
2755
+ code: 421,
2756
+ message: "Misdirected Request"
2757
+ },
2758
+ {
2759
+ code: 422,
2760
+ message: "Unprocessable Entity"
2761
+ },
2762
+ {
2763
+ code: 423,
2764
+ message: "Locked"
2765
+ },
2766
+ {
2767
+ code: 424,
2768
+ message: "Failed Dependency"
2769
+ },
2770
+ {
2771
+ code: 425,
2772
+ message: "Too Early"
2773
+ },
2774
+ {
2775
+ code: 426,
2776
+ message: "Upgrade Required"
2777
+ },
2778
+ {
2779
+ code: 428,
2780
+ message: "Precondition Required"
2781
+ },
2782
+ {
2783
+ code: 429,
2784
+ message: "Too Many Requests"
2785
+ },
2786
+ {
2787
+ code: 431,
2788
+ message: "Request Header Fields Too Large"
2789
+ },
2790
+ {
2791
+ code: 451,
2792
+ message: "Unavailable For Legal Reasons"
2793
+ },
2794
+ {
2795
+ code: 500,
2796
+ message: "Internal Server Error"
2797
+ },
2798
+ {
2799
+ code: 501,
2800
+ message: "Not Implemented"
2801
+ },
2802
+ {
2803
+ code: 502,
2804
+ message: "Bad Gateway"
2805
+ },
2806
+ {
2807
+ code: 503,
2808
+ message: "Service Unavailable"
2809
+ },
2810
+ {
2811
+ code: 504,
2812
+ message: "Gateway Timeout"
2813
+ },
2814
+ {
2815
+ code: 505,
2816
+ message: "HTTP Version Not Supported"
2817
+ },
2818
+ {
2819
+ code: 506,
2820
+ message: "Variant Also Negotiates"
2821
+ },
2822
+ {
2823
+ code: 507,
2824
+ message: "Insufficient Storage"
2825
+ },
2826
+ {
2827
+ code: 508,
2828
+ message: "Loop Detected"
2829
+ },
2830
+ {
2831
+ code: 510,
2832
+ message: "Not Extended"
2833
+ },
2834
+ {
2835
+ code: 511,
2836
+ message: "Network Authentication Required"
2837
+ }
2838
+ ];
2839
+ const statusSchema = {
2840
+ description: "Return status code or random status code if more than one are given ",
2841
+ tags: ["Status Codes"],
2842
+ params: {
2843
+ type: "object",
2844
+ properties: { code: {
2845
+ type: "string",
2846
+ description: "HTTP status code to be returned"
2847
+ } }
2848
+ },
2849
+ response: { 200: {
2850
+ type: "object",
2851
+ properties: { status: { type: "string" } }
2852
+ } }
2853
+ };
2854
+ const statusCodeRoute = (fastify) => {
2855
+ const handler = async (request, reply) => {
2856
+ let { code } = request.params;
2857
+ if (!code || !statusCodes.map((status) => status.code).includes(Number(code))) code = statusCodes[randomInt(0, statusCodes.length)].code.toString();
2858
+ await reply.code(Number(code)).send({ status: `Response with status code ${code}` });
2859
+ };
2860
+ fastify.get("/status/:code?", { schema: statusSchema }, handler);
2861
+ fastify.post("/status/:code?", { schema: statusSchema }, handler);
2862
+ fastify.put("/status/:code?", { schema: statusSchema }, handler);
2863
+ fastify.patch("/status/:code?", { schema: statusSchema }, handler);
2864
+ fastify.delete("/status/:code?", { schema: statusSchema }, handler);
2865
+ };
2866
+ const fastifySwaggerConfig = { openapi: {
2867
+ info: {
2868
+ title: "Mock HTTP API",
2869
+ description: `
2870
+ A simple HTTP server that can be used to mock HTTP responses for testing purposes. Inspired by [httpbin](https://httpbin.org/) and built using \`nodejs\` and \`fastify\` with the idea of running it via https://mockhttp.org, via docker \`jaredwray/mockhttp\`, or nodejs \`npm install jaredwray/mockhttp\`.
2871
+
2872
+ * [GitHub Repository](https://github.com/jaredwray/mockhttp)
2873
+ * [Docker Image](https://hub.docker.com/r/jaredwray/mockhttp)
2874
+ * [NPM Package](https://www.npmjs.com/package/jaredwray/mockhttp)
2875
+
2876
+ # About mockhttp.org
2877
+
2878
+ [mockhttp.org](https://mockhttp.org) is a free service that runs this codebase and allows you to use it for testing purposes. It is a simple way to mock HTTP responses for testing purposes. Ran via [Cloudflare](https://cloudflare.com) and [Google Cloud Run](https://cloud.google.com/run/) across 7 regions globally and can do millions of requests per second.
2879
+ `,
2880
+ version: "1.5.1"
2881
+ },
2882
+ consumes: ["application/json"],
2883
+ produces: ["application/json"]
2884
+ } };
2885
+ const registerSwaggerUi = async (fastify) => {
2886
+ await fastify.register(fastifySwaggerUi, {
2887
+ routePrefix: "/docs",
2888
+ uiConfig: {
2889
+ docExpansion: "none",
2890
+ deepLinking: false
2891
+ },
2892
+ uiHooks: {
2893
+ onRequest(_request, _reply, next) {
2894
+ next();
2895
+ },
2896
+ preHandler(_request, _reply, next) {
2897
+ next();
2898
+ }
2899
+ },
2900
+ staticCSP: true,
2901
+ transformSpecification: (swaggerObject, _request, _reply) => swaggerObject,
2902
+ transformSpecificationClone: true
2903
+ });
2904
+ };
2905
+ //#endregion
2906
+ //#region src/tap-manager.ts
2907
+ /**
2908
+ * Manages HTTP response injections for testing and mocking
2909
+ */
2910
+ var TapManager = class {
2911
+ _injections;
2912
+ constructor() {
2913
+ this._injections = /* @__PURE__ */ new Map();
2914
+ }
2915
+ /**
2916
+ * Get all active injection taps
2917
+ */
2918
+ get injections() {
2919
+ return this._injections;
2920
+ }
2921
+ /**
2922
+ * Check if there are any active injections
2923
+ */
2924
+ get hasInjections() {
2925
+ return this._injections.size > 0;
2926
+ }
2927
+ /**
2928
+ * Inject a custom response for requests matching the given criteria
2929
+ * @param response - The response configuration or a function that returns it
2930
+ * @param matcher - Optional criteria to match requests
2931
+ * @returns A tap object that can be used to remove the injection
2932
+ */
2933
+ inject(response, matcher) {
2934
+ const id = randomUUID();
2935
+ const tap = {
2936
+ id,
2937
+ response,
2938
+ matcher
2939
+ };
2940
+ this._injections.set(id, tap);
2941
+ return tap;
2942
+ }
2943
+ /**
2944
+ * Remove an injection by its tap object or ID
2945
+ * @param tapOrId - The tap object or tap ID to remove
2946
+ * @returns true if the injection was removed, false if it wasn't found
2947
+ */
2948
+ removeInjection(tapOrId) {
2949
+ const id = typeof tapOrId === "string" ? tapOrId : tapOrId.id;
2950
+ return this._injections.delete(id);
2951
+ }
2952
+ /**
2953
+ * Find the first injection that matches the given request
2954
+ * @param request - The Fastify request object
2955
+ * @returns The matching injection tap, or undefined if no match
2956
+ */
2957
+ matchRequest(request) {
2958
+ for (const tap of this._injections.values()) if (this.requestMatches(request, tap.matcher)) return tap;
2959
+ }
2960
+ /**
2961
+ * Clear all injections
2962
+ */
2963
+ clear() {
2964
+ this._injections.clear();
2965
+ }
2966
+ /**
2967
+ * Check if a request matches the given matcher criteria
2968
+ * @param request - The Fastify request object
2969
+ * @param matcher - The matcher criteria (undefined matches all requests)
2970
+ * @returns true if the request matches
2971
+ */
2972
+ requestMatches(request, matcher) {
2973
+ if (!matcher) return true;
2974
+ if (matcher.url) {
2975
+ if (!this.urlMatches(request.url, matcher.url)) return false;
2976
+ }
2977
+ if (matcher.hostname) {
2978
+ if (request.hostname !== matcher.hostname) return false;
2979
+ }
2980
+ if (matcher.method) {
2981
+ if (request.method.toUpperCase() !== matcher.method.toUpperCase()) return false;
2982
+ }
2983
+ if (matcher.headers) {
2984
+ for (const [key, value] of Object.entries(matcher.headers)) if (request.headers[key.toLowerCase()] !== value) return false;
2985
+ }
2986
+ return true;
2987
+ }
2988
+ /**
2989
+ * Check if a URL matches a pattern (supports wildcards with *)
2990
+ * @param url - The URL to test
2991
+ * @param pattern - The pattern to match against
2992
+ * @returns true if the URL matches the pattern
2993
+ */
2994
+ urlMatches(url, pattern) {
2995
+ const urlPath = url.split("?")[0];
2996
+ if (pattern === urlPath) return true;
2997
+ if (pattern.includes("*")) {
2998
+ const regexPattern = pattern.split("*").map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
2999
+ return new RegExp(`^${regexPattern}$`).test(urlPath);
3000
+ }
3001
+ return false;
3002
+ }
3003
+ };
3004
+ //#endregion
3005
+ //#region src/mock-http.ts
3006
+ var MockHttp = class extends Hookified {
3007
+ _port = 3e3;
3008
+ _host = "0.0.0.0";
3009
+ _autoDetectPort = true;
3010
+ _helmet = true;
3011
+ _apiDocs = true;
3012
+ _logging = true;
3013
+ _httpBin = {
3014
+ httpMethods: true,
3015
+ redirects: true,
3016
+ requestInspection: true,
3017
+ responseInspection: true,
3018
+ responseFormats: true,
3019
+ statusCodes: true,
3020
+ cookies: true,
3021
+ anything: true,
3022
+ auth: true,
3023
+ images: true,
3024
+ dynamicData: true
3025
+ };
3026
+ _rateLimit = {
3027
+ max: 1e3,
3028
+ timeWindow: "1 minute",
3029
+ allowList: ["127.0.0.1", "::1"]
3030
+ };
3031
+ _http2 = false;
3032
+ _http1 = true;
3033
+ _https;
3034
+ _httpsCredentials;
3035
+ _server = Fastify();
3036
+ _taps = new TapManager();
3037
+ constructor(options) {
3038
+ super(options?.hookOptions);
3039
+ if (options?.port) this._port = options.port;
3040
+ if (options?.host) this._host = options.host;
3041
+ if (options?.helmet !== void 0) this._helmet = options.helmet;
3042
+ if (options?.apiDocs !== void 0) this._apiDocs = options.apiDocs;
3043
+ if (options?.httpBin !== void 0) this._httpBin = options.httpBin;
3044
+ if (options?.rateLimit !== void 0) if (options.rateLimit === false) this._rateLimit = void 0;
3045
+ else this._rateLimit = options.rateLimit;
3046
+ if (options?.logging !== void 0) this._logging = options.logging;
3047
+ if (options?.http2 !== void 0) this._http2 = options.http2;
3048
+ if (options?.http1 !== void 0) this._http1 = options.http1;
3049
+ if (options?.https !== void 0) if (options.https === true) this._https = { autoGenerate: true };
3050
+ else if (options.https === false) this._https = void 0;
3051
+ else this._https = options.https;
3052
+ }
3053
+ /**
3054
+ * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
3055
+ * @default 3000
3056
+ */
3057
+ get port() {
3058
+ return this._port;
3059
+ }
3060
+ /**
3061
+ * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
3062
+ * @default 3000
3063
+ */
3064
+ set port(port) {
3065
+ this._port = port;
3066
+ }
3067
+ /**
3068
+ * The host to listen on. If not provided, defaults to 'localhost'.
3069
+ * @default 'localhost'
3070
+ */
3071
+ get host() {
3072
+ return this._host;
3073
+ }
3074
+ /**
3075
+ * The host to listen on. If not provided, defaults to 'localhost'.
3076
+ * @default 'localhost'
3077
+ */
3078
+ set host(host) {
3079
+ this._host = host;
3080
+ }
3081
+ /**
3082
+ * Whether to automatically detect the next available port if the provided port is in use. Defaults to true.
3083
+ * @default true
3084
+ */
3085
+ get autoDetectPort() {
3086
+ return this._autoDetectPort;
3087
+ }
3088
+ /**
3089
+ * Whether to automatically detect the next available port if the provided port is in use. Defaults to true.
3090
+ * @default true
3091
+ */
3092
+ set autoDetectPort(autoDetectPort) {
3093
+ this._autoDetectPort = autoDetectPort;
3094
+ }
3095
+ /**
3096
+ * Whether to use Helmet for security headers. Defaults to true.
3097
+ * @default true
3098
+ */
3099
+ get helmet() {
3100
+ return this._helmet;
3101
+ }
3102
+ /**
3103
+ * Whether to use Helmet for security headers. Defaults to true.
3104
+ * @default true
3105
+ */
3106
+ set helmet(helmet) {
3107
+ this._helmet = helmet;
3108
+ }
3109
+ /**
3110
+ * Whether to use Swagger for API documentation. Defaults to true.
3111
+ * @default true
3112
+ */
3113
+ get apiDocs() {
3114
+ return this._apiDocs;
3115
+ }
3116
+ /**
3117
+ * Whether to use Swagger for API documentation. Defaults to true.
3118
+ * @default true
3119
+ */
3120
+ set apiDocs(apiDocs) {
3121
+ this._apiDocs = apiDocs;
3122
+ }
3123
+ /**
3124
+ * Whether to enable logging. Defaults to true.
3125
+ * @default true
3126
+ */
3127
+ get logging() {
3128
+ return this._logging;
3129
+ }
3130
+ /**
3131
+ * Whether to enable logging. Defaults to true.
3132
+ * @default true
3133
+ */
3134
+ set logging(logging) {
3135
+ this._logging = logging;
3136
+ }
3137
+ /**
3138
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
3139
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
3140
+ */
3141
+ get https() {
3142
+ return this._https;
3143
+ }
3144
+ /**
3145
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
3146
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
3147
+ */
3148
+ set https(value) {
3149
+ if (value === true) this._https = { autoGenerate: true };
3150
+ else if (value === false || value === void 0) this._https = void 0;
3151
+ else this._https = value;
3152
+ }
3153
+ /**
3154
+ * Whether the server is running in HTTPS mode.
3155
+ */
3156
+ get isHttps() {
3157
+ return this._httpsCredentials !== void 0;
3158
+ }
3159
+ /**
3160
+ * Whether HTTP/2 support is enabled.
3161
+ * @default false
3162
+ */
3163
+ get http2() {
3164
+ return this._http2;
3165
+ }
3166
+ /**
3167
+ * Whether HTTP/2 support is enabled.
3168
+ * @default false
3169
+ */
3170
+ set http2(http2) {
3171
+ this._http2 = http2;
3172
+ }
3173
+ /**
3174
+ * Whether to allow HTTP/1.1 fallback when using HTTP/2 with HTTPS.
3175
+ * @default true
3176
+ */
3177
+ get http1() {
3178
+ return this._http1;
3179
+ }
3180
+ /**
3181
+ * Whether to allow HTTP/1.1 fallback when using HTTP/2 with HTTPS.
3182
+ * @default true
3183
+ */
3184
+ set http1(http1) {
3185
+ this._http1 = http1;
3186
+ }
3187
+ /**
3188
+ * HTTP Bin options. Defaults to all enabled.
3189
+ */
3190
+ get httpBin() {
3191
+ return this._httpBin;
3192
+ }
3193
+ /**
3194
+ * HTTP Bin options. Defaults to all enabled.
3195
+ */
3196
+ set httpBin(httpBinary) {
3197
+ this._httpBin = httpBinary;
3198
+ }
3199
+ /**
3200
+ * Rate limiting options. Defaults to 1000 requests per minute (localhost excluded).
3201
+ * Set to undefined to disable rate limiting, or provide custom options to configure.
3202
+ * @default { max: 1000, timeWindow: "1 minute", allowList: ["127.0.0.1", "::1"] }
3203
+ */
3204
+ get rateLimit() {
3205
+ return this._rateLimit;
3206
+ }
3207
+ /**
3208
+ * Rate limiting options. Defaults to 1000 requests per minute (localhost excluded).
3209
+ * Set to undefined to disable rate limiting, or provide custom options to configure.
3210
+ *
3211
+ * Note: Changing this property requires restarting the server (close() then start()) for changes to take effect.
3212
+ * @default { max: 1000, timeWindow: "1 minute", allowList: ["127.0.0.1", "::1"] }
3213
+ */
3214
+ set rateLimit(rateLimit) {
3215
+ this._rateLimit = rateLimit;
3216
+ }
3217
+ /**
3218
+ * The Fastify server instance.
3219
+ */
3220
+ get server() {
3221
+ return this._server;
3222
+ }
3223
+ /**
3224
+ * The Fastify server instance.
3225
+ */
3226
+ set server(server) {
3227
+ this._server = server;
3228
+ }
3229
+ /**
3230
+ * The TapManager instance for managing injection taps.
3231
+ */
3232
+ get taps() {
3233
+ return this._taps;
3234
+ }
3235
+ /**
3236
+ * The TapManager instance for managing injection taps.
3237
+ */
3238
+ set taps(taps) {
3239
+ this._taps = taps;
3240
+ }
3241
+ /**
3242
+ * Start the Fastify server. If the server is already running, it will be closed and restarted.
3243
+ */
3244
+ async start() {
3245
+ this._httpsCredentials = void 0;
3246
+ if (this._https) this._httpsCredentials = await this.resolveHttpsCredentials(this._https);
3247
+ try {
3248
+ /* v8 ignore next -- @preserve */
3249
+ if (this._server) await this._server.close();
3250
+ const fastifyOptions = { ...getFastifyConfig(this._logging) };
3251
+ if (this._http2) fastifyOptions.http2 = true;
3252
+ if (this._httpsCredentials) {
3253
+ const httpsOptions = {
3254
+ key: this._httpsCredentials.key,
3255
+ cert: this._httpsCredentials.cert
3256
+ };
3257
+ if (this._http2) httpsOptions.allowHTTP1 = this._http1;
3258
+ fastifyOptions.https = httpsOptions;
3259
+ }
3260
+ this._server = Fastify(fastifyOptions);
3261
+ this._server.addHook("onRequest", async (request, reply) => {
3262
+ const matchedTap = this._taps.matchRequest(request);
3263
+ if (matchedTap) {
3264
+ const { response, statusCode = 200, headers } = typeof matchedTap.response === "function" ? matchedTap.response(request) : matchedTap.response;
3265
+ reply.code(statusCode);
3266
+ if (headers) for (const [key, value] of Object.entries(headers)) reply.header(key, value);
3267
+ return reply.send(response);
3268
+ }
3269
+ });
3270
+ await this._server.register(fastifyStatic, {
3271
+ root: path.resolve("./node_modules/@scalar/api-reference/dist"),
3272
+ prefix: "/scalar"
3273
+ });
3274
+ await this.server.register(fastifyStatic, {
3275
+ root: path.resolve("./public"),
3276
+ decorateReply: false
3277
+ });
3278
+ await this._server.register(sitemapRoute);
3279
+ if (this._helmet) await this._server.register(fastifyHelmet, { contentSecurityPolicy: { directives: {
3280
+ defaultSrc: ["'self'"],
3281
+ scriptSrc: [
3282
+ "'self'",
3283
+ "'unsafe-inline'",
3284
+ "'wasm-unsafe-eval'"
3285
+ ],
3286
+ styleSrc: ["'self'", "'unsafe-inline'"],
3287
+ imgSrc: [
3288
+ "'self'",
3289
+ "data:",
3290
+ "https:"
3291
+ ],
3292
+ fontSrc: ["'self'", "data:"],
3293
+ connectSrc: ["'self'"],
3294
+ frameSrc: ["'self'"],
3295
+ objectSrc: ["'none'"],
3296
+ upgradeInsecureRequests: []
3297
+ } } });
3298
+ if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
3299
+ if (this._apiDocs) await this.registerApiDocs();
3300
+ const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData } = this._httpBin;
3301
+ if (httpMethods) await this.registerHttpMethods();
3302
+ if (statusCodes) await this.registerStatusCodeRoutes();
3303
+ if (requestInspection) await this.registerRequestInspectionRoutes();
3304
+ if (responseInspection) await this.registerResponseInspectionRoutes();
3305
+ if (responseFormats) await this.registerResponseFormatRoutes();
3306
+ if (redirects) await this.registerRedirectRoutes();
3307
+ if (cookies) await this.registerCookieRoutes();
3308
+ if (anything) await this.registerAnythingRoutes();
3309
+ if (auth) await this.registerAuthRoutes();
3310
+ if (images) await this.registerImageRoutes();
3311
+ if (dynamicData) await this.registerDynamicDataRoutes();
3312
+ if (this._autoDetectPort) {
3313
+ const originalPort = this._port;
3314
+ this._port = await this.detectPort();
3315
+ if (originalPort !== this._port) this._server.log.info(`Port ${originalPort} is in use, detected next available port: ${this._port}`);
3316
+ }
3317
+ await this._server.listen({
3318
+ port: this._port,
3319
+ host: this._host
3320
+ });
3321
+ } catch (error) {
3322
+ /* v8 ignore next -- @preserve */
3323
+ this._server.log.error(error);
3324
+ }
3325
+ }
3326
+ /**
3327
+ * Close the Fastify server.
3328
+ */
3329
+ async close() {
3330
+ await this._server.close();
3331
+ }
3332
+ /**
3333
+ * Detect the next available port to run on.
3334
+ * @returns The port that is available to run on
3335
+ */
3336
+ async detectPort() {
3337
+ const { port } = this;
3338
+ return detect(port);
3339
+ }
3340
+ /**
3341
+ * This will register the API documentation routes including openapi and swagger ui.
3342
+ * @param fastifyInstance - the server instance to register the routes on.
3343
+ */
3344
+ async registerApiDocs(fastifyInstance) {
3345
+ const fastify = fastifyInstance ?? this._server;
3346
+ await fastify.register(fastifySwagger, fastifySwaggerConfig);
3347
+ await registerSwaggerUi(fastify);
3348
+ await fastify.register(indexRoute);
3349
+ }
3350
+ /**
3351
+ * Register the HTTP methods routes.
3352
+ * @param fastifyInstance - the server instance to register the routes on.
3353
+ */
3354
+ async registerHttpMethods(fastifyInstance) {
3355
+ const fastify = fastifyInstance ?? this._server;
3356
+ await fastify.register(getRoute);
3357
+ await fastify.register(postRoute);
3358
+ await fastify.register(deleteRoute);
3359
+ await fastify.register(putRoute);
3360
+ await fastify.register(patchRoute);
3361
+ }
3362
+ /**
3363
+ * Register the status code routes.
3364
+ * @param fastifyInstance - the server instance to register the routes on.
3365
+ */
3366
+ async registerStatusCodeRoutes(fastifyInstance) {
3367
+ await (fastifyInstance ?? this._server).register(statusCodeRoute);
3368
+ }
3369
+ /**
3370
+ * Register the request inspection routes.
3371
+ * @param fastifyInstance - the server instance to register the routes on.
3372
+ */
3373
+ async registerRequestInspectionRoutes(fastifyInstance) {
3374
+ const fastify = fastifyInstance ?? this._server;
3375
+ await fastify.register(ipRoute);
3376
+ await fastify.register(headersRoute);
3377
+ await fastify.register(userAgentRoute);
3378
+ }
3379
+ /**
3380
+ * Register the response inspection routes.
3381
+ * @param fastifyInstance - the server instance to register the routes on.
3382
+ */
3383
+ async registerResponseInspectionRoutes(fastifyInstance) {
3384
+ const fastify = fastifyInstance ?? this._server;
3385
+ await fastify.register(cacheRoutes);
3386
+ await fastify.register(etagRoutes);
3387
+ await fastify.register(responseHeadersRoutes);
3388
+ }
3389
+ async registerResponseFormatRoutes(fastifyInstance) {
3390
+ await (fastifyInstance ?? this._server).register(responseFormatRoutes);
3391
+ }
3392
+ /**
3393
+ * Register the redirect routes.
3394
+ * @param fastifyInstance - the server instance to register the routes on.
3395
+ */
3396
+ async registerRedirectRoutes(fastifyInstance) {
3397
+ const fastify = fastifyInstance ?? this._server;
3398
+ await fastify.register(absoluteRedirectRoute);
3399
+ await fastify.register(relativeRedirectRoute);
3400
+ await fastify.register(redirectToRoute);
3401
+ }
3402
+ /**
3403
+ * Register the cookie routes.
3404
+ * @param fastifyInstance - the server instance to register the routes on.
3405
+ */
3406
+ async registerCookieRoutes(fastifyInstance) {
3407
+ const fastify = fastifyInstance ?? this._server;
3408
+ await fastify.register(fastifyCookie);
3409
+ await fastify.register(getCookiesRoute);
3410
+ await fastify.register(postCookieRoute);
3411
+ await fastify.register(deleteCookieRoute);
3412
+ }
3413
+ /**
3414
+ * Register the anything routes.
3415
+ * @param fastifyInstance - the server instance to register the routes on.
3416
+ */
3417
+ async registerAnythingRoutes(fastifyInstance) {
3418
+ await (fastifyInstance ?? this._server).register(anythingRoute);
3419
+ }
3420
+ /**
3421
+ * Register the auth routes.
3422
+ * @param fastifyInstance - the server instance to register the routes on.
3423
+ */
3424
+ async registerAuthRoutes(fastifyInstance) {
3425
+ const fastify = fastifyInstance ?? this._server;
3426
+ await fastify.register(basicAuthRoute);
3427
+ await fastify.register(hiddenBasicAuthRoute);
3428
+ await fastify.register(bearerAuthRoute);
3429
+ await fastify.register(digestAuthRoute);
3430
+ }
3431
+ /**
3432
+ * Register the image routes.
3433
+ * @param fastifyInstance - the server instance to register the routes on.
3434
+ */
3435
+ async registerImageRoutes(fastifyInstance) {
3436
+ await (fastifyInstance ?? this._server).register(imageRoutes);
3437
+ }
3438
+ /**
3439
+ * Register the dynamic data routes.
3440
+ * @param fastifyInstance - the server instance to register the routes on.
3441
+ */
3442
+ async registerDynamicDataRoutes(fastifyInstance) {
3443
+ const fastify = fastifyInstance ?? this._server;
3444
+ await fastify.register(uuidRoute);
3445
+ await fastify.register(bytesRoute);
3446
+ await fastify.register(streamBytesRoute);
3447
+ await fastify.register(delayRoute);
3448
+ await fastify.register(base64Route);
3449
+ await fastify.register(streamRoute);
3450
+ await fastify.register(rangeRoute);
3451
+ await fastify.register(dripRoute);
3452
+ await fastify.register(linksRoute);
3453
+ }
3454
+ async resolveHttpsCredentials(options) {
3455
+ if (options.cert && options.key) return {
3456
+ cert: await this.loadPemValue(options.cert),
3457
+ key: await this.loadPemValue(options.key)
3458
+ };
3459
+ if ((options.cert || options.key) && options.autoGenerate !== true) throw new Error("HTTPS options must include both 'cert' and 'key'. Only one was provided.");
3460
+ if (options.autoGenerate !== false) {
3461
+ const result = generateCertificate(options.certificateOptions);
3462
+ return {
3463
+ cert: result.cert,
3464
+ key: result.key
3465
+ };
3466
+ }
3467
+ throw new Error("HTTPS is enabled but no certificate was provided and autoGenerate is false.");
3468
+ }
3469
+ async loadPemValue(value) {
3470
+ if (value.startsWith("-----")) return value;
3471
+ return fsPromises.readFile(value, "utf8");
3472
+ }
3473
+ };
3474
+ //#endregion
3475
+ //#region src/index.ts
3476
+ const start = async () => {
3477
+ const mockHttp = new MockHttp();
3478
+ /* v8 ignore next -- @preserve */
3479
+ if (process$1.env.PORT) mockHttp.port = Number.parseInt(process$1.env.PORT, 10);
3480
+ /* v8 ignore next -- @preserve */
3481
+ if (process$1.env.HOST) mockHttp.host = process$1.env.HOST;
3482
+ /* v8 ignore next -- @preserve */
3483
+ if (process$1.env.LOGGING === "false") mockHttp.logging = false;
3484
+ /* v8 ignore next -- @preserve */
3485
+ if (process$1.env.HTTP2 === "true") mockHttp.http2 = true;
3486
+ await mockHttp.start();
3487
+ return mockHttp;
3488
+ };
3489
+ /* v8 ignore next -- @preserve */
3490
+ if (import.meta.url === `file://${process$1.argv[1]}`) await start();
3491
+ //#endregion
3492
+ export { MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };