@jaredwray/mockhttp 1.8.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +29 -7
  2. package/dist/index.d.mts +135 -33
  3. package/dist/index.mjs +360 -342
  4. package/package.json +22 -18
  5. package/site/dist/api/index.html +17983 -0
  6. package/site/dist/api/swagger.json +5487 -0
  7. package/site/dist/changelog/index.html +767 -0
  8. package/site/dist/changelog/page/2/index.html +578 -0
  9. package/site/dist/changelog/v0-1-0/index.html +453 -0
  10. package/site/dist/changelog/v0-2-0/index.html +437 -0
  11. package/site/dist/changelog/v0-2-1/index.html +438 -0
  12. package/site/dist/changelog/v0-2-2/index.html +436 -0
  13. package/site/dist/changelog/v0-2-3/index.html +440 -0
  14. package/site/dist/changelog/v0-3-0/index.html +443 -0
  15. package/site/dist/changelog/v0-4-0/index.html +437 -0
  16. package/site/dist/changelog/v0-4-1/index.html +441 -0
  17. package/site/dist/changelog/v0-4-2/index.html +446 -0
  18. package/site/dist/changelog/v0-4-3/index.html +437 -0
  19. package/site/dist/changelog/v0-5-0/index.html +443 -0
  20. package/site/dist/changelog/v0-6-0/index.html +444 -0
  21. package/site/dist/changelog/v0-6-1/index.html +441 -0
  22. package/site/dist/changelog/v0-7-0/index.html +445 -0
  23. package/site/dist/changelog/v0-8-0/index.html +440 -0
  24. package/site/dist/changelog/v0-9-0/index.html +437 -0
  25. package/site/dist/changelog/v1-0-0/index.html +446 -0
  26. package/site/dist/changelog/v1-0-1/index.html +437 -0
  27. package/site/dist/changelog/v1-1-0/index.html +445 -0
  28. package/site/dist/changelog/v1-2-0/index.html +443 -0
  29. package/site/dist/changelog/v1-3-0/index.html +448 -0
  30. package/site/dist/changelog/v1-4-0/index.html +445 -0
  31. package/site/dist/changelog/v1-5-0/index.html +439 -0
  32. package/site/dist/changelog/v1-5-1/index.html +443 -0
  33. package/site/dist/changelog/v1-6-0/index.html +467 -0
  34. package/site/dist/changelog/v1-7-0/index.html +462 -0
  35. package/site/dist/changelog/v1-8-0/index.html +481 -0
  36. package/site/dist/changelog/v1-8-1/index.html +457 -0
  37. package/site/dist/changelog/v1-9-0/index.html +495 -0
  38. package/site/dist/changelog/v2-0-0/index.html +500 -0
  39. package/site/dist/changelog-latest.json +269 -0
  40. package/site/dist/changelog.json +399 -0
  41. package/site/dist/css/api.css +1113 -0
  42. package/site/dist/css/highlight/highlight.min.js +1433 -0
  43. package/site/dist/css/highlight/styles/base16/docula.css +123 -0
  44. package/site/dist/css/home.css +133 -0
  45. package/site/dist/css/styles.css +1227 -0
  46. package/site/dist/css/variables.css +32 -0
  47. package/site/dist/docs/bins/index.html +942 -0
  48. package/site/dist/docs/configuration/index.html +771 -0
  49. package/site/dist/docs/hosted/index.html +544 -0
  50. package/site/dist/docs/https/index.html +758 -0
  51. package/site/dist/docs/index.html +601 -0
  52. package/site/dist/docs/library/index.html +780 -0
  53. package/site/dist/docs/taps/index.html +708 -0
  54. package/site/dist/favicon.svg +14 -0
  55. package/site/dist/feed.xml +1 -0
  56. package/site/dist/index.html +601 -0
  57. package/site/dist/js/api.js +363 -0
  58. package/site/dist/js/search.js +436 -0
  59. package/site/dist/llms-full.txt +7415 -0
  60. package/site/dist/llms.txt +40 -0
  61. package/site/dist/logo.svg +21 -0
  62. package/site/dist/robots.txt +2 -0
  63. package/site/dist/search-index.json +1 -0
  64. package/site/dist/sitemap.xml +1 -0
package/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import process$1 from "node:process";
2
+ import { existsSync, readFileSync } from "node:fs";
2
3
  import * as fsPromises from "node:fs/promises";
3
4
  import path, { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
  import fastifyCookie from "@fastify/cookie";
5
7
  import fastifyHelmet from "@fastify/helmet";
6
8
  import fastifyRateLimit from "@fastify/rate-limit";
@@ -12,10 +14,8 @@ import { Hookified } from "hookified";
12
14
  import * as crypto$1 from "node:crypto";
13
15
  import crypto, { randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
14
16
  import { Buffer as Buffer$1 } from "node:buffer";
15
- import { readFileSync } from "node:fs";
16
17
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
17
18
  import { escape } from "html-escaper";
18
- import { fastifySwaggerUi } from "@fastify/swagger-ui";
19
19
  //#region src/bin-store.ts
20
20
  /**
21
21
  * In-memory implementation of {@link BinStore} backed by two Maps. State is
@@ -70,9 +70,9 @@ var InMemoryBinStore = class {
70
70
  };
71
71
  //#endregion
72
72
  //#region src/bin-manager.ts
73
- const DEFAULT_TTL_MS = 1440 * 60 * 1e3;
73
+ const DEFAULT_TTL_MS = 864e5;
74
74
  const DEFAULT_MAX_REQUESTS = 100;
75
- const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
75
+ const DEFAULT_MAX_BODY_SIZE = 1048576;
76
76
  const DEFAULT_ID_LENGTH = 12;
77
77
  const DEFAULT_CLEANUP_INTERVAL_MS = 6e4;
78
78
  /**
@@ -188,7 +188,7 @@ var BinManager = class {
188
188
  this._cleanupTimer = setInterval(() => {
189
189
  this._store.cleanupExpired(Date.now());
190
190
  }, this._cleanupIntervalMs);
191
- this._cleanupTimer.unref();
191
+ this._cleanupTimer.unref?.();
192
192
  }
193
193
  /** Stop the periodic cleanup. Idempotent. */
194
194
  stop() {
@@ -228,18 +228,19 @@ function encodeSet(...elements) {
228
228
  }
229
229
  function encodeInteger(value) {
230
230
  let buf;
231
- if (typeof value === "number") if (value === 0) buf = Buffer.from([0]);
232
- else {
233
- const bytes = [];
234
- let temp = value;
235
- while (temp > 0) {
236
- bytes.unshift(temp & 255);
237
- temp >>= 8;
238
- }
239
- if (bytes[0] & 128) bytes.unshift(0);
240
- buf = Buffer.from(bytes);
241
- }
242
- else if (value.length > 0 && value[0] & 128) buf = Buffer.concat([Buffer.from([0]), value]);
231
+ if (typeof value === "number") {
232
+ if (value === 0) buf = Buffer.from([0]);
233
+ else {
234
+ const bytes = [];
235
+ let temp = value;
236
+ while (temp > 0) {
237
+ bytes.unshift(temp & 255);
238
+ temp >>= 8;
239
+ }
240
+ if (bytes[0] & 128) bytes.unshift(0);
241
+ buf = Buffer.from(bytes);
242
+ }
243
+ } else if (value.length > 0 && value[0] & 128) buf = Buffer.concat([Buffer.from([0]), value]);
243
244
  else buf = value;
244
245
  return encodeTlv(2, buf);
245
246
  }
@@ -690,7 +691,7 @@ const bearerAuthRoute = (fastify) => {
690
691
  //#endregion
691
692
  //#region src/routes/auth/digest.ts
692
693
  const makeNonce = () => crypto.randomBytes(16).toString("hex");
693
- const algorithms = new Set([
694
+ const algorithms = /* @__PURE__ */ new Set([
694
695
  "MD5",
695
696
  "md5",
696
697
  "SHA-256",
@@ -1374,7 +1375,7 @@ function generateSeededBytes$1(n, seed) {
1374
1375
  for (let i = 0; i < n; i++) bytes[i] = Math.floor(random() * 256);
1375
1376
  return bytes;
1376
1377
  }
1377
- const MAX_BYTES$3 = 100 * 1024;
1378
+ const MAX_BYTES$3 = 102400;
1378
1379
  const bytesRoute = (fastify) => {
1379
1380
  fastify.get("/bytes/:n", { schema: bytesSchema }, async (request, reply) => {
1380
1381
  const n = Number.parseInt(request.params.n, 10);
@@ -1501,7 +1502,7 @@ const DEFAULT_CODE = 200;
1501
1502
  const DEFAULT_DELAY = 2;
1502
1503
  const MAX_DURATION = 10;
1503
1504
  const MAX_DELAY = 10;
1504
- const MAX_BYTES$2 = 10 * 1024 * 1024;
1505
+ const MAX_BYTES$2 = 10485760;
1505
1506
  const dripRoute = (fastify) => {
1506
1507
  fastify.get("/drip", { schema: dripSchema }, async (request, reply) => {
1507
1508
  /* v8 ignore next 2 -- @preserve */
@@ -1627,7 +1628,7 @@ const rangeSchema = {
1627
1628
  }
1628
1629
  }
1629
1630
  };
1630
- const MAX_BYTES$1 = 100 * 1024;
1631
+ const MAX_BYTES$1 = 102400;
1631
1632
  function sleep(ms) {
1632
1633
  return new Promise((resolve) => setTimeout(resolve, ms));
1633
1634
  }
@@ -1808,7 +1809,7 @@ function generateSeededBytes(n, seed) {
1808
1809
  for (let i = 0; i < n; i++) bytes[i] = Math.floor(random() * 256);
1809
1810
  return bytes;
1810
1811
  }
1811
- const MAX_BYTES = 100 * 1024;
1812
+ const MAX_BYTES = 102400;
1812
1813
  const DEFAULT_CHUNK_SIZE = 10240;
1813
1814
  const streamBytesRoute = (fastify) => {
1814
1815
  fastify.get("/stream-bytes/:n", { schema: streamBytesSchema }, async (request, reply) => {
@@ -2042,6 +2043,18 @@ const putRoute = (fastify) => {
2042
2043
  }));
2043
2044
  };
2044
2045
  //#endregion
2046
+ //#region src/public-files.ts
2047
+ const defaultPublicPath$1 = join(process.cwd(), "public");
2048
+ const defaultReader = (file) => readFileSync(join(defaultPublicPath$1, file));
2049
+ let reader = defaultReader;
2050
+ /**
2051
+ * Read a file from the package `public/` directory, or from the active reader.
2052
+ */
2053
+ async function readPublicFile(file) {
2054
+ const bytes = await reader(file);
2055
+ return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
2056
+ }
2057
+ //#endregion
2045
2058
  //#region src/routes/images/index.ts
2046
2059
  const imageSchema = {
2047
2060
  description: "Returns an image based on Accept header (supports jpeg, png, svg, webp)",
@@ -2095,8 +2108,19 @@ const webpSchema = {
2095
2108
  format: "binary"
2096
2109
  } }
2097
2110
  };
2111
+ async function sendImage(reply, file, contentType) {
2112
+ try {
2113
+ const imageBuffer = await readPublicFile(file);
2114
+ reply.type(contentType);
2115
+ return imageBuffer;
2116
+ } catch (error) {
2117
+ /* v8 ignore next -- @preserve */
2118
+ reply.code(500);
2119
+ /* v8 ignore next -- @preserve */
2120
+ return { error: `${error}` };
2121
+ }
2122
+ }
2098
2123
  const imageRoutes = (fastify) => {
2099
- const publicPath = join(process.cwd(), "public");
2100
2124
  fastify.get("/image", { schema: imageSchema }, async (request, reply) => {
2101
2125
  const accept = request.headers.accept || "";
2102
2126
  let file = "logo.png";
@@ -2114,147 +2138,19 @@ const imageRoutes = (fastify) => {
2114
2138
  file = "logo.png";
2115
2139
  contentType = "image/png";
2116
2140
  }
2117
- try {
2118
- const imageBuffer = readFileSync(join(publicPath, file));
2119
- reply.type(contentType);
2120
- return imageBuffer;
2121
- } catch (error) {
2122
- /* v8 ignore next -- @preserve */
2123
- reply.code(500);
2124
- /* v8 ignore next -- @preserve */
2125
- return { error: `${error}` };
2126
- }
2141
+ return sendImage(reply, file, contentType);
2127
2142
  });
2128
2143
  fastify.get("/image/jpeg", { schema: jpegSchema }, async (_request, reply) => {
2129
- try {
2130
- const imageBuffer = readFileSync(join(publicPath, "logo.jpg"));
2131
- reply.type("image/jpeg");
2132
- return imageBuffer;
2133
- } catch (error) {
2134
- /* v8 ignore next -- @preserve */
2135
- reply.code(500);
2136
- /* v8 ignore next -- @preserve */
2137
- return { error: `${error}` };
2138
- }
2144
+ return sendImage(reply, "logo.jpg", "image/jpeg");
2139
2145
  });
2140
2146
  fastify.get("/image/png", { schema: pngSchema }, async (_request, reply) => {
2141
- try {
2142
- const imageBuffer = readFileSync(join(publicPath, "logo.png"));
2143
- reply.type("image/png");
2144
- return imageBuffer;
2145
- } catch (error) {
2146
- /* v8 ignore next -- @preserve */
2147
- reply.code(500);
2148
- /* v8 ignore next -- @preserve */
2149
- return { error: `${error}` };
2150
- }
2147
+ return sendImage(reply, "logo.png", "image/png");
2151
2148
  });
2152
2149
  fastify.get("/image/svg", { schema: svgSchema }, async (_request, reply) => {
2153
- try {
2154
- const imageBuffer = readFileSync(join(publicPath, "logo.svg"));
2155
- reply.type("image/svg+xml");
2156
- return imageBuffer;
2157
- } catch (error) {
2158
- /* v8 ignore next -- @preserve */
2159
- reply.code(500);
2160
- /* v8 ignore next -- @preserve */
2161
- return { error: `${error}` };
2162
- }
2150
+ return sendImage(reply, "logo.svg", "image/svg+xml");
2163
2151
  });
2164
2152
  fastify.get("/image/webp", { schema: webpSchema }, async (_request, reply) => {
2165
- try {
2166
- const imageBuffer = readFileSync(join(publicPath, "logo.webp"));
2167
- reply.type("image/webp");
2168
- return imageBuffer;
2169
- } catch (error) {
2170
- /* v8 ignore next -- @preserve */
2171
- reply.code(500);
2172
- /* v8 ignore next -- @preserve */
2173
- return { error: `${error}` };
2174
- }
2175
- });
2176
- };
2177
- //#endregion
2178
- //#region src/routes/index.ts
2179
- const indexRoute = (fastify) => {
2180
- fastify.get("/", { schema: { hide: true } }, async (_request, reply) => {
2181
- await reply.type("text/html").send(`
2182
- <!doctype html>
2183
- <html>
2184
- <head>
2185
- <title>{ mockhttp }</title>
2186
- <meta charset="utf-8" />
2187
- <meta
2188
- name="viewport"
2189
- content="width=device-width, initial-scale=1" />
2190
- <meta name="description" content="MockHttp - A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
2191
- <meta name="keywords" content="mock, http, httpbin, testing, api, fastify, docker, typescript, rest, mock-server, api-testing, http-testing, httpbin-alternative" />
2192
- <meta property="og:title" content="MockHttp - HTTP Mock Server" />
2193
- <meta property="og:description" content="A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
2194
- <meta property="og:type" content="website" />
2195
- <meta property="og:url" content="https://mockhttp.org" />
2196
- <meta property="og:image" content="https://mockhttp.org/logo.png" />
2197
- <meta property="og:site_name" content="MockHttp" />
2198
- <meta name="twitter:card" content="summary" />
2199
- <meta name="twitter:title" content="MockHttp - HTTP Mock Server" />
2200
- <meta name="twitter:description" content="A powerful HTTP mock server and httpbin replacement for API testing." />
2201
- <meta name="twitter:image" content="https://mockhttp.org/logo.png" />
2202
- <style>
2203
- :root {
2204
- --scalar-custom-header-height: 70px;
2205
- }
2206
- .custom-header {
2207
- height: var(--scalar-custom-header-height);
2208
- background-color: var(--scalar-background-1);
2209
- box-shadow: inset 0 -1px 0 var(--scalar-border-color);
2210
- color: var(--scalar-color-1);
2211
- font-size: var(--scalar-font-size-2);
2212
- padding: 0 18px;
2213
- position: sticky;
2214
- justify-content: space-between;
2215
- top: 0;
2216
- z-index: 100;
2217
- }
2218
- .custom-header,
2219
- .custom-header nav {
2220
- display: flex;
2221
- align-items: center;
2222
- gap: 18px;
2223
- }
2224
-
2225
- .custom-header img {
2226
- height: var(--scalar-custom-header-height);
2227
- object-fit: contain;
2228
- }
2229
-
2230
- .custom-header a:hover {
2231
- color: var(--scalar-color-2);
2232
- }
2233
-
2234
- .github-corner {
2235
- fill: var(--scalar-border-color) !important;
2236
- }
2237
-
2238
- </style>
2239
- </head>
2240
- <body>
2241
- <header class="custom-header scalar-app">
2242
- <a href="/"><img src="/logo.svg" alt="{ mockhttp }" /></a>
2243
- <a href="https://github.com/jaredwray/mockhttp" class="github-corner" aria-label="View source on GitHub">
2244
- <svg width="70" height="70" viewBox="0 0 250 250" style="color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
2245
- <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
2246
- <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>
2247
- <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>
2248
- </header>
2249
-
2250
- <script
2251
- id="api-reference"
2252
- data-url="/docs/json"><\/script>
2253
-
2254
- <script src="/scalar/browser/standalone.js"><\/script>
2255
- </body>
2256
- </html>
2257
- `);
2153
+ return sendImage(reply, "logo.webp", "image/webp");
2258
2154
  });
2259
2155
  };
2260
2156
  //#endregion
@@ -2438,46 +2334,78 @@ const userAgentRoute = (fastify) => {
2438
2334
  };
2439
2335
  //#endregion
2440
2336
  //#region src/routes/response-formats/index.ts
2441
- const plainSchema = {
2442
- description: "Returns random plain text content",
2443
- tags: ["Response Formats"],
2444
- response: { 200: {
2337
+ const templateIndexParams = {
2338
+ type: "object",
2339
+ properties: { index: {
2445
2340
  type: "string",
2446
- description: "Random plain text content"
2341
+ description: "Optional 1-based template index for stable, repeatable output. Omit for a random template."
2447
2342
  } }
2448
2343
  };
2344
+ const templateNotFoundResponse = {
2345
+ type: "object",
2346
+ description: "The requested template index does not exist",
2347
+ properties: { error: { type: "string" } }
2348
+ };
2349
+ const plainSchema = {
2350
+ description: "Returns plain text content. A random template is selected unless a 1-based template index is provided (e.g. /plain/1) for stable output.",
2351
+ tags: ["Response Formats"],
2352
+ params: templateIndexParams,
2353
+ response: {
2354
+ 200: {
2355
+ type: "string",
2356
+ description: "Plain text content"
2357
+ },
2358
+ 404: templateNotFoundResponse
2359
+ }
2360
+ };
2449
2361
  const textSchema = {
2450
- description: "Returns random text content",
2362
+ description: "Returns text content. A random template is selected unless a 1-based template index is provided (e.g. /text/1) for stable output.",
2451
2363
  tags: ["Response Formats"],
2452
- response: { 200: {
2453
- type: "string",
2454
- description: "Random text content"
2455
- } }
2364
+ params: templateIndexParams,
2365
+ response: {
2366
+ 200: {
2367
+ type: "string",
2368
+ description: "Text content"
2369
+ },
2370
+ 404: templateNotFoundResponse
2371
+ }
2456
2372
  };
2457
2373
  const htmlSchema = {
2458
- description: "Returns random HTML content",
2374
+ description: "Returns HTML content. A random template is selected unless a 1-based template index is provided (e.g. /html/1) for stable output.",
2459
2375
  tags: ["Response Formats"],
2460
- response: { 200: {
2461
- type: "string",
2462
- description: "Random HTML content"
2463
- } }
2376
+ params: templateIndexParams,
2377
+ response: {
2378
+ 200: {
2379
+ type: "string",
2380
+ description: "HTML content"
2381
+ },
2382
+ 404: templateNotFoundResponse
2383
+ }
2464
2384
  };
2465
2385
  const xmlSchema = {
2466
- description: "Returns random XML content",
2386
+ description: "Returns XML content. A random template is selected unless a 1-based template index is provided (e.g. /xml/1) for stable output.",
2467
2387
  tags: ["Response Formats"],
2468
- response: { 200: {
2469
- type: "string",
2470
- description: "Random XML content"
2471
- } }
2388
+ params: templateIndexParams,
2389
+ response: {
2390
+ 200: {
2391
+ type: "string",
2392
+ description: "XML content"
2393
+ },
2394
+ 404: templateNotFoundResponse
2395
+ }
2472
2396
  };
2473
2397
  const jsonSchema = {
2474
- description: "Returns random JSON content",
2398
+ description: "Returns JSON content. A random template is selected unless a 1-based template index is provided (e.g. /json/1) for stable output.",
2475
2399
  tags: ["Response Formats"],
2476
- response: { 200: {
2477
- type: "object",
2478
- description: "Random JSON content",
2479
- additionalProperties: true
2480
- } }
2400
+ params: templateIndexParams,
2401
+ response: {
2402
+ 200: {
2403
+ type: "object",
2404
+ description: "JSON content",
2405
+ additionalProperties: true
2406
+ },
2407
+ 404: templateNotFoundResponse
2408
+ }
2481
2409
  };
2482
2410
  const denySchema = {
2483
2411
  description: "Returns page denied by robots.txt rules",
@@ -2613,7 +2541,7 @@ const randomXmlContent = [
2613
2541
  `<?xml version="1.0" encoding="UTF-8"?>
2614
2542
  <root>
2615
2543
  <message>Hello from MockHTTP</message>
2616
- <timestamp>${(/* @__PURE__ */ new Date()).toISOString()}</timestamp>
2544
+ <timestamp>2024-01-15T10:30:00.000Z</timestamp>
2617
2545
  <status>success</status>
2618
2546
  </root>`,
2619
2547
  `<?xml version="1.0" encoding="UTF-8"?>
@@ -2656,19 +2584,19 @@ const randomXmlContent = [
2656
2584
  <title>First Article</title>
2657
2585
  <link>http://example.com/article1</link>
2658
2586
  <description>This is the first article in our feed</description>
2659
- <pubDate>${(/* @__PURE__ */ new Date()).toUTCString()}</pubDate>
2587
+ <pubDate>Mon, 15 Jan 2024 10:30:00 GMT</pubDate>
2660
2588
  </item>
2661
2589
  </channel>
2662
2590
  </rss>`
2663
2591
  ];
2664
2592
  const randomJsonContent = [
2665
- () => ({
2593
+ {
2666
2594
  message: "Hello from MockHTTP",
2667
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2595
+ timestamp: "2024-01-15T10:30:00.000Z",
2668
2596
  status: "success",
2669
2597
  code: 200
2670
- }),
2671
- () => ({
2598
+ },
2599
+ {
2672
2600
  user: {
2673
2601
  id: 1,
2674
2602
  name: "John Doe",
@@ -2681,8 +2609,8 @@ const randomJsonContent = [
2681
2609
  "write",
2682
2610
  "delete"
2683
2611
  ]
2684
- }),
2685
- () => ({
2612
+ },
2613
+ {
2686
2614
  products: [{
2687
2615
  id: 101,
2688
2616
  name: "Laptop",
@@ -2696,8 +2624,8 @@ const randomJsonContent = [
2696
2624
  }],
2697
2625
  total: 2,
2698
2626
  page: 1
2699
- }),
2700
- () => ({ config: {
2627
+ },
2628
+ { config: {
2701
2629
  theme: "dark",
2702
2630
  language: "en",
2703
2631
  notifications: {
@@ -2709,8 +2637,8 @@ const randomJsonContent = [
2709
2637
  profile: "public",
2710
2638
  activity: "friends"
2711
2639
  }
2712
- } }),
2713
- () => ({ metrics: {
2640
+ } },
2641
+ { metrics: {
2714
2642
  cpu: 45.2,
2715
2643
  memory: 78.5,
2716
2644
  disk: 62.3,
@@ -2733,60 +2661,71 @@ const randomJsonContent = [
2733
2661
  status: "warning"
2734
2662
  }
2735
2663
  ]
2736
- } })
2664
+ } }
2737
2665
  ];
2666
+ const resolveTemplateIndex = (index, length) => {
2667
+ if (index === void 0) return Math.floor(Math.random() * length);
2668
+ const value = Number(index);
2669
+ if (!Number.isInteger(value) || value < 1 || value > length) return -1;
2670
+ return value - 1;
2671
+ };
2672
+ const templateNotFound = (reply, length) => reply.code(404).send({ error: `Template index must be between 1 and ${length}` });
2738
2673
  const responseFormatRoutes = (fastify) => {
2739
- const plainHandler = async (_request, reply) => {
2740
- const text = randomTexts[Math.floor(Math.random() * randomTexts.length)];
2674
+ const plainHandler = async (request, reply) => {
2675
+ const index = resolveTemplateIndex(request.params.index, randomTexts.length);
2676
+ if (index === -1) return templateNotFound(reply, randomTexts.length);
2741
2677
  reply.type("text/plain");
2742
- return text;
2678
+ return randomTexts[index];
2743
2679
  };
2744
- fastify.get("/plain", { schema: plainSchema }, plainHandler);
2745
- fastify.post("/plain", { schema: plainSchema }, plainHandler);
2746
- fastify.put("/plain", { schema: plainSchema }, plainHandler);
2747
- fastify.patch("/plain", { schema: plainSchema }, plainHandler);
2748
- fastify.delete("/plain", { schema: plainSchema }, plainHandler);
2749
- const textHandler = async (_request, reply) => {
2750
- const text = randomTexts[Math.floor(Math.random() * randomTexts.length)];
2680
+ fastify.get("/plain/:index?", { schema: plainSchema }, plainHandler);
2681
+ fastify.post("/plain/:index?", { schema: plainSchema }, plainHandler);
2682
+ fastify.put("/plain/:index?", { schema: plainSchema }, plainHandler);
2683
+ fastify.patch("/plain/:index?", { schema: plainSchema }, plainHandler);
2684
+ fastify.delete("/plain/:index?", { schema: plainSchema }, plainHandler);
2685
+ const textHandler = async (request, reply) => {
2686
+ const index = resolveTemplateIndex(request.params.index, randomTexts.length);
2687
+ if (index === -1) return templateNotFound(reply, randomTexts.length);
2751
2688
  reply.type("text/plain");
2752
- return text;
2689
+ return randomTexts[index];
2753
2690
  };
2754
- fastify.get("/text", { schema: textSchema }, textHandler);
2755
- fastify.post("/text", { schema: textSchema }, textHandler);
2756
- fastify.put("/text", { schema: textSchema }, textHandler);
2757
- fastify.patch("/text", { schema: textSchema }, textHandler);
2758
- fastify.delete("/text", { schema: textSchema }, textHandler);
2759
- const htmlHandler = async (_request, reply) => {
2760
- const html = randomHtmlContent[Math.floor(Math.random() * randomHtmlContent.length)];
2691
+ fastify.get("/text/:index?", { schema: textSchema }, textHandler);
2692
+ fastify.post("/text/:index?", { schema: textSchema }, textHandler);
2693
+ fastify.put("/text/:index?", { schema: textSchema }, textHandler);
2694
+ fastify.patch("/text/:index?", { schema: textSchema }, textHandler);
2695
+ fastify.delete("/text/:index?", { schema: textSchema }, textHandler);
2696
+ const htmlHandler = async (request, reply) => {
2697
+ const index = resolveTemplateIndex(request.params.index, randomHtmlContent.length);
2698
+ if (index === -1) return templateNotFound(reply, randomHtmlContent.length);
2761
2699
  reply.type("text/html");
2762
- return html;
2700
+ return randomHtmlContent[index];
2763
2701
  };
2764
- fastify.get("/html", { schema: htmlSchema }, htmlHandler);
2765
- fastify.post("/html", { schema: htmlSchema }, htmlHandler);
2766
- fastify.put("/html", { schema: htmlSchema }, htmlHandler);
2767
- fastify.patch("/html", { schema: htmlSchema }, htmlHandler);
2768
- fastify.delete("/html", { schema: htmlSchema }, htmlHandler);
2769
- const xmlHandler = async (_request, reply) => {
2770
- const xml = randomXmlContent[Math.floor(Math.random() * randomXmlContent.length)];
2702
+ fastify.get("/html/:index?", { schema: htmlSchema }, htmlHandler);
2703
+ fastify.post("/html/:index?", { schema: htmlSchema }, htmlHandler);
2704
+ fastify.put("/html/:index?", { schema: htmlSchema }, htmlHandler);
2705
+ fastify.patch("/html/:index?", { schema: htmlSchema }, htmlHandler);
2706
+ fastify.delete("/html/:index?", { schema: htmlSchema }, htmlHandler);
2707
+ const xmlHandler = async (request, reply) => {
2708
+ const index = resolveTemplateIndex(request.params.index, randomXmlContent.length);
2709
+ if (index === -1) return templateNotFound(reply, randomXmlContent.length);
2771
2710
  reply.type("application/xml");
2772
- return xml;
2711
+ return randomXmlContent[index];
2773
2712
  };
2774
- fastify.get("/xml", { schema: xmlSchema }, xmlHandler);
2775
- fastify.post("/xml", { schema: xmlSchema }, xmlHandler);
2776
- fastify.put("/xml", { schema: xmlSchema }, xmlHandler);
2777
- fastify.patch("/xml", { schema: xmlSchema }, xmlHandler);
2778
- fastify.delete("/xml", { schema: xmlSchema }, xmlHandler);
2779
- const jsonHandler = async (_request, reply) => {
2780
- const jsonGenerator = randomJsonContent[Math.floor(Math.random() * randomJsonContent.length)];
2781
- const json = jsonGenerator();
2713
+ fastify.get("/xml/:index?", { schema: xmlSchema }, xmlHandler);
2714
+ fastify.post("/xml/:index?", { schema: xmlSchema }, xmlHandler);
2715
+ fastify.put("/xml/:index?", { schema: xmlSchema }, xmlHandler);
2716
+ fastify.patch("/xml/:index?", { schema: xmlSchema }, xmlHandler);
2717
+ fastify.delete("/xml/:index?", { schema: xmlSchema }, xmlHandler);
2718
+ const jsonHandler = async (request, reply) => {
2719
+ const index = resolveTemplateIndex(request.params.index, randomJsonContent.length);
2720
+ if (index === -1) return templateNotFound(reply, randomJsonContent.length);
2782
2721
  reply.type("application/json");
2783
- return json;
2722
+ return structuredClone(randomJsonContent[index]);
2784
2723
  };
2785
- fastify.get("/json", { schema: jsonSchema }, jsonHandler);
2786
- fastify.post("/json", { schema: jsonSchema }, jsonHandler);
2787
- fastify.put("/json", { schema: jsonSchema }, jsonHandler);
2788
- fastify.patch("/json", { schema: jsonSchema }, jsonHandler);
2789
- fastify.delete("/json", { schema: jsonSchema }, jsonHandler);
2724
+ fastify.get("/json/:index?", { schema: jsonSchema }, jsonHandler);
2725
+ fastify.post("/json/:index?", { schema: jsonSchema }, jsonHandler);
2726
+ fastify.put("/json/:index?", { schema: jsonSchema }, jsonHandler);
2727
+ fastify.patch("/json/:index?", { schema: jsonSchema }, jsonHandler);
2728
+ fastify.delete("/json/:index?", { schema: jsonSchema }, jsonHandler);
2790
2729
  const denyHandler = async (_request, reply) => {
2791
2730
  const denyMessages = [
2792
2731
  `<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
@@ -2869,7 +2808,8 @@ const responseFormatRoutes = (fastify) => {
2869
2808
  ...data,
2870
2809
  compressedSize: compressed.length
2871
2810
  };
2872
- const updatedCompressed = gzipSync(JSON.stringify(updatedData));
2811
+ const updatedJsonString = JSON.stringify(updatedData);
2812
+ const updatedCompressed = gzipSync(updatedJsonString);
2873
2813
  reply.header("Content-Encoding", "gzip");
2874
2814
  reply.header("Content-Type", "application/json");
2875
2815
  reply.type("application/octet-stream");
@@ -2925,7 +2865,8 @@ const responseFormatRoutes = (fastify) => {
2925
2865
  ...data,
2926
2866
  compressedSize: compressed.length
2927
2867
  };
2928
- const updatedCompressed = deflateSync(JSON.stringify(updatedData));
2868
+ const updatedJsonString = JSON.stringify(updatedData);
2869
+ const updatedCompressed = deflateSync(updatedJsonString);
2929
2870
  reply.header("Content-Encoding", "deflate");
2930
2871
  reply.header("Content-Type", "application/json");
2931
2872
  reply.type("application/octet-stream");
@@ -2986,7 +2927,8 @@ const responseFormatRoutes = (fastify) => {
2986
2927
  ...data,
2987
2928
  compressedSize: compressed.length
2988
2929
  };
2989
- const updatedCompressed = brotliCompressSync(JSON.stringify(updatedData));
2930
+ const updatedJsonString = JSON.stringify(updatedData);
2931
+ const updatedCompressed = brotliCompressSync(updatedJsonString);
2990
2932
  reply.header("Content-Encoding", "br");
2991
2933
  reply.header("Content-Type", "application/json");
2992
2934
  reply.type("application/octet-stream");
@@ -3130,29 +3072,6 @@ const responseHeadersRoutes = (fastify) => {
3130
3072
  });
3131
3073
  };
3132
3074
  //#endregion
3133
- //#region src/routes/sitemap.ts
3134
- const sitemapRoute = (fastify) => {
3135
- fastify.get("/sitemap.xml", { schema: { hide: true } }, async (_request, reply) => {
3136
- try {
3137
- const host = _request.headers.host ?? "mockhttp.org";
3138
- const xml = `<?xml version="1.0" encoding="UTF-8"?>
3139
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3140
- <url>
3141
- <loc>${_request.headers["x-forwarded-proto"]?.includes("https") ? "https" : "http"}://${host}/</loc>
3142
- <changefreq>monthly</changefreq>
3143
- <priority>1.0</priority>
3144
- </url>
3145
- </urlset>`;
3146
- await reply.type("text/xml").send(xml);
3147
- } catch (error) {
3148
- /* v8 ignore next -- @preserve */
3149
- fastify.log.error(error);
3150
- /* v8 ignore next -- @preserve */
3151
- await reply.status(500).send({ error: "Internal Server Error" });
3152
- }
3153
- });
3154
- };
3155
- //#endregion
3156
3075
  //#region src/routes/status-codes/index.ts
3157
3076
  const statusCodes = [
3158
3077
  {
@@ -3435,47 +3354,30 @@ const statusCodeRoute = (fastify) => {
3435
3354
  fastify.patch("/status/:code?", { schema: statusSchema }, handler);
3436
3355
  fastify.delete("/status/:code?", { schema: statusSchema }, handler);
3437
3356
  };
3438
- const fastifySwaggerConfig = { openapi: {
3439
- info: {
3440
- title: "Mock HTTP API",
3441
- description: `
3357
+ const fastifySwaggerConfig = { openapi: { info: {
3358
+ title: "Mock HTTP API",
3359
+ description: `
3442
3360
  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\`.
3443
3361
 
3444
3362
  * [GitHub Repository](https://github.com/jaredwray/mockhttp)
3445
3363
  * [Docker Image](https://hub.docker.com/r/jaredwray/mockhttp)
3446
3364
  * [NPM Package](https://www.npmjs.com/package/@jaredwray/mockhttp)
3447
3365
 
3448
- # About mockhttp.org
3366
+ # About mockhttp.org
3449
3367
 
3450
- [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.
3368
+ [mockhttp.org](https://mockhttp.org) is a free hosted instance of this codebase for testing. It runs on a Cloudflare Worker (no containers): documentation is served from Workers Assets, and mock APIs such as \`/get\` and \`/post\` run in the Worker. The service is globally available and rate-limited (1000 requests per minute per IP) to prevent abuse.
3451
3369
  `,
3452
- version: "1.8.1"
3453
- },
3454
- consumes: ["application/json"],
3455
- produces: ["application/json"]
3456
- } };
3457
- const registerSwaggerUi = async (fastify) => {
3458
- await fastify.register(fastifySwaggerUi, {
3459
- routePrefix: "/docs",
3460
- uiConfig: {
3461
- docExpansion: "none",
3462
- deepLinking: false
3463
- },
3464
- uiHooks: {
3465
- /* v8 ignore next -- @preserve */
3466
- onRequest(_request, _reply, next) {
3467
- next();
3468
- },
3469
- /* v8 ignore next -- @preserve */
3470
- preHandler(_request, _reply, next) {
3471
- next();
3472
- }
3473
- },
3474
- staticCSP: true,
3475
- /* v8 ignore next -- @preserve */
3476
- transformSpecification: (swaggerObject, _request, _reply) => swaggerObject,
3477
- transformSpecificationClone: true
3478
- });
3370
+ version: "2.0.0"
3371
+ } } };
3372
+ const withLocalServer = (spec) => {
3373
+ spec.servers = [{
3374
+ url: "",
3375
+ description: "This instance"
3376
+ }];
3377
+ return spec;
3378
+ };
3379
+ const registerOpenApiJson = async (fastify) => {
3380
+ fastify.get("/openapi.json", { schema: { hide: true } }, async () => withLocalServer(fastify.swagger()));
3479
3381
  };
3480
3382
  //#endregion
3481
3383
  //#region src/tap-manager.ts
@@ -3578,13 +3480,29 @@ var TapManager = class {
3578
3480
  };
3579
3481
  //#endregion
3580
3482
  //#region src/mock-http.ts
3483
+ function resolvePackageRoot(metaUrl) {
3484
+ if (!metaUrl) return process.cwd();
3485
+ return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..");
3486
+ }
3487
+ const packageRoot = resolvePackageRoot(import.meta.url);
3488
+ const defaultSiteDistPath = path.join(packageRoot, "site", "dist");
3489
+ const defaultPublicPath = path.join(packageRoot, "public");
3490
+ const requestOrigin = (request) => {
3491
+ const host = request.headers.host || "mockhttp.org";
3492
+ const forwarded = request.headers["x-forwarded-proto"];
3493
+ return `${((Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? request.protocol).includes("https") ? "https" : "http"}://${host}`;
3494
+ };
3581
3495
  var MockHttp = class extends Hookified {
3582
3496
  _port = 3e3;
3583
3497
  _host = "0.0.0.0";
3584
3498
  _autoDetectPort = true;
3585
3499
  _helmet = true;
3500
+ _staticFiles = true;
3586
3501
  _apiDocs = true;
3502
+ _siteDistPath = defaultSiteDistPath;
3587
3503
  _logging = true;
3504
+ _pluginTimeout;
3505
+ _startBins = true;
3588
3506
  _httpBin = {
3589
3507
  httpMethods: true,
3590
3508
  redirects: true,
@@ -3608,24 +3526,33 @@ var MockHttp = class extends Hookified {
3608
3526
  _http1 = true;
3609
3527
  _https;
3610
3528
  _httpsCredentials;
3611
- _server = Fastify();
3529
+ _server;
3612
3530
  _taps = new TapManager();
3613
3531
  _bins = new BinManager();
3614
3532
  constructor(options) {
3615
3533
  super(options?.hookOptions);
3616
3534
  if (options?.port) this._port = options.port;
3617
3535
  if (options?.host) this._host = options.host;
3536
+ if (options?.autoDetectPort !== void 0) this._autoDetectPort = options.autoDetectPort;
3618
3537
  if (options?.helmet !== void 0) this._helmet = options.helmet;
3538
+ if (options?.staticFiles !== void 0) this._staticFiles = options.staticFiles;
3619
3539
  if (options?.apiDocs !== void 0) this._apiDocs = options.apiDocs;
3540
+ if (options?.siteDistPath !== void 0) this._siteDistPath = path.resolve(options.siteDistPath);
3620
3541
  if (options?.httpBin !== void 0) this._httpBin = options.httpBin;
3621
- if (options?.rateLimit !== void 0) if (options.rateLimit === false) this._rateLimit = void 0;
3622
- else this._rateLimit = options.rateLimit;
3542
+ if (options?.rateLimit !== void 0) {
3543
+ if (options.rateLimit === false) this._rateLimit = void 0;
3544
+ else this._rateLimit = options.rateLimit;
3545
+ }
3623
3546
  if (options?.logging !== void 0) this._logging = options.logging;
3547
+ if (options?.pluginTimeout !== void 0) this._pluginTimeout = options.pluginTimeout;
3548
+ if (options?.startBins !== void 0) this._startBins = options.startBins;
3624
3549
  if (options?.http2 !== void 0) this._http2 = options.http2;
3625
3550
  if (options?.http1 !== void 0) this._http1 = options.http1;
3626
- if (options?.https !== void 0) if (options.https === true) this._https = { autoGenerate: true };
3627
- else if (options.https === false) this._https = void 0;
3628
- else this._https = options.https;
3551
+ if (options?.https !== void 0) {
3552
+ if (options.https === true) this._https = { autoGenerate: true };
3553
+ else if (options.https === false) this._https = void 0;
3554
+ else this._https = options.https;
3555
+ }
3629
3556
  }
3630
3557
  /**
3631
3558
  * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
@@ -3684,20 +3611,48 @@ var MockHttp = class extends Hookified {
3684
3611
  this._helmet = helmet;
3685
3612
  }
3686
3613
  /**
3687
- * Whether to use Swagger for API documentation. Defaults to true.
3614
+ * Whether to serve files from the package `public/` directory. Defaults to true.
3615
+ * @default true
3616
+ */
3617
+ get staticFiles() {
3618
+ return this._staticFiles;
3619
+ }
3620
+ /**
3621
+ * Whether to serve files from the package `public/` directory. Defaults to true.
3622
+ * @default true
3623
+ */
3624
+ set staticFiles(staticFiles) {
3625
+ this._staticFiles = staticFiles;
3626
+ }
3627
+ /**
3628
+ * Whether to serve the Docula documentation site and OpenAPI spec. Defaults to true.
3688
3629
  * @default true
3689
3630
  */
3690
3631
  get apiDocs() {
3691
3632
  return this._apiDocs;
3692
3633
  }
3693
3634
  /**
3694
- * Whether to use Swagger for API documentation. Defaults to true.
3635
+ * Whether to serve the Docula documentation site and OpenAPI spec. Defaults to true.
3695
3636
  * @default true
3696
3637
  */
3697
3638
  set apiDocs(apiDocs) {
3698
3639
  this._apiDocs = apiDocs;
3699
3640
  }
3700
3641
  /**
3642
+ * Path to the built Docula site served when `apiDocs` is enabled.
3643
+ * @default the package `site/dist` directory
3644
+ */
3645
+ get siteDistPath() {
3646
+ return this._siteDistPath;
3647
+ }
3648
+ /**
3649
+ * Path to the built Docula site served when `apiDocs` is enabled.
3650
+ * @default the package `site/dist` directory
3651
+ */
3652
+ set siteDistPath(siteDistPath) {
3653
+ this._siteDistPath = path.resolve(siteDistPath);
3654
+ }
3655
+ /**
3701
3656
  * Whether to enable logging. Defaults to true.
3702
3657
  * @default true
3703
3658
  */
@@ -3712,6 +3667,32 @@ var MockHttp = class extends Hookified {
3712
3667
  this._logging = logging;
3713
3668
  }
3714
3669
  /**
3670
+ * Fastify plugin boot timeout in milliseconds. `undefined` uses Fastify's default.
3671
+ */
3672
+ get pluginTimeout() {
3673
+ return this._pluginTimeout;
3674
+ }
3675
+ /**
3676
+ * Fastify plugin boot timeout in milliseconds. `undefined` uses Fastify's default.
3677
+ */
3678
+ set pluginTimeout(pluginTimeout) {
3679
+ this._pluginTimeout = pluginTimeout;
3680
+ }
3681
+ /**
3682
+ * Whether to start the request-bin cleanup timer during `initialize()`.
3683
+ * @default true
3684
+ */
3685
+ get startBins() {
3686
+ return this._startBins;
3687
+ }
3688
+ /**
3689
+ * Whether to start the request-bin cleanup timer during `initialize()`.
3690
+ * @default true
3691
+ */
3692
+ set startBins(startBins) {
3693
+ this._startBins = startBins;
3694
+ }
3695
+ /**
3715
3696
  * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
3716
3697
  * or provide an HttpsOptions object with cert/key or auto-generation options.
3717
3698
  */
@@ -3795,6 +3776,7 @@ var MockHttp = class extends Hookified {
3795
3776
  * The Fastify server instance.
3796
3777
  */
3797
3778
  get server() {
3779
+ this._server ??= Fastify();
3798
3780
  return this._server;
3799
3781
  }
3800
3782
  /**
@@ -3828,15 +3810,17 @@ var MockHttp = class extends Hookified {
3828
3810
  this._bins = bins;
3829
3811
  }
3830
3812
  /**
3831
- * Start the Fastify server. If the server is already running, it will be closed and restarted.
3813
+ * Register plugins and routes without listening. Use `inject()` against
3814
+ * `server`, or call `start()` to bind a port.
3832
3815
  */
3833
- async start() {
3816
+ async initialize() {
3834
3817
  this._httpsCredentials = void 0;
3835
3818
  if (this._https) this._httpsCredentials = await this.resolveHttpsCredentials(this._https);
3836
3819
  try {
3837
3820
  /* v8 ignore next -- @preserve */
3838
- if (this._server) await this._server.close();
3821
+ if (this._server?.server.listening) await this._server.close();
3839
3822
  const fastifyOptions = { ...getFastifyConfig(this._logging) };
3823
+ if (this._pluginTimeout !== void 0) fastifyOptions.pluginTimeout = this._pluginTimeout;
3840
3824
  if (this._http2) fastifyOptions.http2 = true;
3841
3825
  if (this._httpsCredentials) {
3842
3826
  const httpsOptions = {
@@ -3856,22 +3840,14 @@ var MockHttp = class extends Hookified {
3856
3840
  return reply.send(response);
3857
3841
  }
3858
3842
  });
3859
- await this._server.register(fastifyStatic, {
3860
- root: path.resolve("./node_modules/@scalar/api-reference/dist"),
3861
- prefix: "/scalar"
3862
- });
3863
- await this.server.register(fastifyStatic, {
3864
- root: path.resolve("./public"),
3865
- decorateReply: false
3843
+ if (this._staticFiles) await this.server.register(fastifyStatic, {
3844
+ root: defaultPublicPath,
3845
+ wildcard: false,
3846
+ index: false
3866
3847
  });
3867
- await this._server.register(sitemapRoute);
3868
3848
  if (this._helmet) await this._server.register(fastifyHelmet, { contentSecurityPolicy: { directives: {
3869
3849
  defaultSrc: ["'self'"],
3870
- scriptSrc: [
3871
- "'self'",
3872
- "'unsafe-inline'",
3873
- "'wasm-unsafe-eval'"
3874
- ],
3850
+ scriptSrc: ["'self'", "'unsafe-inline'"],
3875
3851
  styleSrc: ["'self'", "'unsafe-inline'"],
3876
3852
  imgSrc: [
3877
3853
  "'self'",
@@ -3879,13 +3855,13 @@ var MockHttp = class extends Hookified {
3879
3855
  "https:"
3880
3856
  ],
3881
3857
  fontSrc: ["'self'", "data:"],
3882
- connectSrc: ["'self'"],
3858
+ connectSrc: ["'self'", "https:"],
3883
3859
  frameSrc: ["'self'"],
3884
3860
  objectSrc: ["'none'"],
3885
3861
  upgradeInsecureRequests: []
3886
3862
  } } });
3887
3863
  if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
3888
- if (this._apiDocs) await this.registerApiDocs();
3864
+ if (this._apiDocs) await this.registerSwagger();
3889
3865
  const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData, bins } = this._httpBin;
3890
3866
  if (httpMethods) await this.registerHttpMethods();
3891
3867
  if (statusCodes) await this.registerStatusCodeRoutes();
@@ -3900,20 +3876,32 @@ var MockHttp = class extends Hookified {
3900
3876
  if (dynamicData) await this.registerDynamicDataRoutes();
3901
3877
  if (bins) {
3902
3878
  await this.registerBinRoutes();
3903
- this._bins.start();
3879
+ if (this._startBins) this._bins.start();
3904
3880
  }
3881
+ if (this._apiDocs) await this.registerSite();
3882
+ } catch (error) {
3883
+ /* v8 ignore next -- @preserve */
3884
+ this.server.log.error(error);
3885
+ }
3886
+ }
3887
+ /**
3888
+ * Start the Fastify server. If the server is already running, it will be closed and restarted.
3889
+ */
3890
+ async start() {
3891
+ await this.initialize();
3892
+ try {
3905
3893
  if (this._autoDetectPort) {
3906
3894
  const originalPort = this._port;
3907
3895
  this._port = await this.detectPort();
3908
- if (originalPort !== this._port) this._server.log.info(`Port ${originalPort} is in use, detected next available port: ${this._port}`);
3896
+ if (originalPort !== this._port) this.server.log.info(`Port ${originalPort} is in use, detected next available port: ${this._port}`);
3909
3897
  }
3910
- await this._server.listen({
3898
+ await this.server.listen({
3911
3899
  port: this._port,
3912
3900
  host: this._host
3913
3901
  });
3914
3902
  } catch (error) {
3915
3903
  /* v8 ignore next -- @preserve */
3916
- this._server.log.error(error);
3904
+ this.server.log.error(error);
3917
3905
  }
3918
3906
  }
3919
3907
  /**
@@ -3921,7 +3909,7 @@ var MockHttp = class extends Hookified {
3921
3909
  */
3922
3910
  async close() {
3923
3911
  this._bins.stop();
3924
- await this._server.close();
3912
+ if (this._server) await this._server.close();
3925
3913
  }
3926
3914
  /**
3927
3915
  * Detect the next available port to run on.
@@ -3932,21 +3920,51 @@ var MockHttp = class extends Hookified {
3932
3920
  return detect(port);
3933
3921
  }
3934
3922
  /**
3935
- * This will register the API documentation routes including openapi and swagger ui.
3923
+ * Register OpenAPI generation and the Docula documentation site.
3936
3924
  * @param fastifyInstance - the server instance to register the routes on.
3937
3925
  */
3938
3926
  async registerApiDocs(fastifyInstance) {
3939
- const fastify = fastifyInstance ?? this._server;
3927
+ await this.registerSwagger(fastifyInstance);
3928
+ await this.registerSite(fastifyInstance);
3929
+ }
3930
+ /**
3931
+ * Register `@fastify/swagger` and the live `/openapi.json` spec route.
3932
+ * @param fastifyInstance - the server instance to register the routes on.
3933
+ */
3934
+ async registerSwagger(fastifyInstance) {
3935
+ const fastify = fastifyInstance ?? this.server;
3940
3936
  await fastify.register(fastifySwagger, fastifySwaggerConfig);
3941
- await registerSwaggerUi(fastify);
3942
- await fastify.register(indexRoute);
3937
+ await registerOpenApiJson(fastify);
3938
+ }
3939
+ /**
3940
+ * Serve the built Docula site from `siteDistPath` when the directory exists.
3941
+ * @param fastifyInstance - the server instance to register the routes on.
3942
+ */
3943
+ async registerSite(fastifyInstance) {
3944
+ const fastify = fastifyInstance ?? this.server;
3945
+ if (!existsSync(this._siteDistPath)) {
3946
+ fastify.log.warn(`Docula site not found at ${this._siteDistPath}; skipping static docs`);
3947
+ return;
3948
+ }
3949
+ const sitemapPath = path.join(this._siteDistPath, "sitemap.xml");
3950
+ if (existsSync(sitemapPath)) fastify.get("/sitemap.xml", { schema: { hide: true } }, async (request, reply) => {
3951
+ const xml = await fsPromises.readFile(sitemapPath, "utf8");
3952
+ return reply.type("application/xml").send(xml.replaceAll("https://mockhttp.org", requestOrigin(request)));
3953
+ });
3954
+ await fastify.register(fastifyStatic, {
3955
+ root: this._siteDistPath,
3956
+ prefix: "/",
3957
+ decorateReply: !fastify.hasReplyDecorator("sendFile"),
3958
+ index: ["index.html"],
3959
+ wildcard: true
3960
+ });
3943
3961
  }
3944
3962
  /**
3945
3963
  * Register the HTTP methods routes.
3946
3964
  * @param fastifyInstance - the server instance to register the routes on.
3947
3965
  */
3948
3966
  async registerHttpMethods(fastifyInstance) {
3949
- const fastify = fastifyInstance ?? this._server;
3967
+ const fastify = fastifyInstance ?? this.server;
3950
3968
  await fastify.register(getRoute);
3951
3969
  await fastify.register(postRoute);
3952
3970
  await fastify.register(deleteRoute);
@@ -3958,14 +3976,14 @@ var MockHttp = class extends Hookified {
3958
3976
  * @param fastifyInstance - the server instance to register the routes on.
3959
3977
  */
3960
3978
  async registerStatusCodeRoutes(fastifyInstance) {
3961
- await (fastifyInstance ?? this._server).register(statusCodeRoute);
3979
+ await (fastifyInstance ?? this.server).register(statusCodeRoute);
3962
3980
  }
3963
3981
  /**
3964
3982
  * Register the request inspection routes.
3965
3983
  * @param fastifyInstance - the server instance to register the routes on.
3966
3984
  */
3967
3985
  async registerRequestInspectionRoutes(fastifyInstance) {
3968
- const fastify = fastifyInstance ?? this._server;
3986
+ const fastify = fastifyInstance ?? this.server;
3969
3987
  await fastify.register(ipRoute);
3970
3988
  await fastify.register(headersRoute);
3971
3989
  await fastify.register(userAgentRoute);
@@ -3975,20 +3993,20 @@ var MockHttp = class extends Hookified {
3975
3993
  * @param fastifyInstance - the server instance to register the routes on.
3976
3994
  */
3977
3995
  async registerResponseInspectionRoutes(fastifyInstance) {
3978
- const fastify = fastifyInstance ?? this._server;
3996
+ const fastify = fastifyInstance ?? this.server;
3979
3997
  await fastify.register(cacheRoutes);
3980
3998
  await fastify.register(etagRoutes);
3981
3999
  await fastify.register(responseHeadersRoutes);
3982
4000
  }
3983
4001
  async registerResponseFormatRoutes(fastifyInstance) {
3984
- await (fastifyInstance ?? this._server).register(responseFormatRoutes);
4002
+ await (fastifyInstance ?? this.server).register(responseFormatRoutes);
3985
4003
  }
3986
4004
  /**
3987
4005
  * Register the redirect routes.
3988
4006
  * @param fastifyInstance - the server instance to register the routes on.
3989
4007
  */
3990
4008
  async registerRedirectRoutes(fastifyInstance) {
3991
- const fastify = fastifyInstance ?? this._server;
4009
+ const fastify = fastifyInstance ?? this.server;
3992
4010
  await fastify.register(absoluteRedirectRoute);
3993
4011
  await fastify.register(relativeRedirectRoute);
3994
4012
  await fastify.register(redirectToRoute);
@@ -3998,7 +4016,7 @@ var MockHttp = class extends Hookified {
3998
4016
  * @param fastifyInstance - the server instance to register the routes on.
3999
4017
  */
4000
4018
  async registerCookieRoutes(fastifyInstance) {
4001
- const fastify = fastifyInstance ?? this._server;
4019
+ const fastify = fastifyInstance ?? this.server;
4002
4020
  await fastify.register(fastifyCookie);
4003
4021
  await fastify.register(getCookiesRoute);
4004
4022
  await fastify.register(postCookieRoute);
@@ -4009,14 +4027,14 @@ var MockHttp = class extends Hookified {
4009
4027
  * @param fastifyInstance - the server instance to register the routes on.
4010
4028
  */
4011
4029
  async registerAnythingRoutes(fastifyInstance) {
4012
- await (fastifyInstance ?? this._server).register(anythingRoute);
4030
+ await (fastifyInstance ?? this.server).register(anythingRoute);
4013
4031
  }
4014
4032
  /**
4015
4033
  * Register the auth routes.
4016
4034
  * @param fastifyInstance - the server instance to register the routes on.
4017
4035
  */
4018
4036
  async registerAuthRoutes(fastifyInstance) {
4019
- const fastify = fastifyInstance ?? this._server;
4037
+ const fastify = fastifyInstance ?? this.server;
4020
4038
  await fastify.register(basicAuthRoute);
4021
4039
  await fastify.register(hiddenBasicAuthRoute);
4022
4040
  await fastify.register(bearerAuthRoute);
@@ -4027,14 +4045,14 @@ var MockHttp = class extends Hookified {
4027
4045
  * @param fastifyInstance - the server instance to register the routes on.
4028
4046
  */
4029
4047
  async registerImageRoutes(fastifyInstance) {
4030
- await (fastifyInstance ?? this._server).register(imageRoutes);
4048
+ await (fastifyInstance ?? this.server).register(imageRoutes);
4031
4049
  }
4032
4050
  /**
4033
4051
  * Register the dynamic data routes.
4034
4052
  * @param fastifyInstance - the server instance to register the routes on.
4035
4053
  */
4036
4054
  async registerDynamicDataRoutes(fastifyInstance) {
4037
- const fastify = fastifyInstance ?? this._server;
4055
+ const fastify = fastifyInstance ?? this.server;
4038
4056
  await fastify.register(uuidRoute);
4039
4057
  await fastify.register(bytesRoute);
4040
4058
  await fastify.register(streamBytesRoute);
@@ -4050,7 +4068,7 @@ var MockHttp = class extends Hookified {
4050
4068
  * @param fastifyInstance - the server instance to register the routes on.
4051
4069
  */
4052
4070
  async registerBinRoutes(fastifyInstance) {
4053
- const fastify = fastifyInstance ?? this._server;
4071
+ const fastify = fastifyInstance ?? this.server;
4054
4072
  await fastify.register(binsManagementRoute(this._bins));
4055
4073
  await fastify.register(binsCaptureRoute(this._bins));
4056
4074
  }