@jaredwray/mockhttp 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -13,195 +13,10 @@ import Fastify from "fastify";
13
13
  import { Hookified } from "hookified";
14
14
  import * as crypto$1 from "node:crypto";
15
15
  import crypto, { randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
16
+ import Keyv from "keyv";
16
17
  import { Buffer as Buffer$1 } from "node:buffer";
17
18
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
18
19
  import { escape } from "html-escaper";
19
- //#region src/bin-store.ts
20
- /**
21
- * In-memory implementation of {@link BinStore} backed by two Maps. State is
22
- * lost when the process exits.
23
- */
24
- var InMemoryBinStore = class {
25
- _bins = /* @__PURE__ */ new Map();
26
- _requests = /* @__PURE__ */ new Map();
27
- createBin(bin) {
28
- this._bins.set(bin.id, bin);
29
- this._requests.set(bin.id, []);
30
- }
31
- getBin(id) {
32
- return this._bins.get(id);
33
- }
34
- listBins() {
35
- return [...this._bins.values()];
36
- }
37
- deleteBin(id) {
38
- this._requests.delete(id);
39
- return this._bins.delete(id);
40
- }
41
- addRequest(binId, req, max) {
42
- const bin = this._bins.get(binId);
43
- const list = this._requests.get(binId);
44
- if (!bin || !list) return;
45
- list.unshift(req);
46
- while (list.length > max && list.length > 0) list.pop();
47
- bin.requestCount = list.length;
48
- }
49
- getRequests(binId) {
50
- return [...this._requests.get(binId) ?? []];
51
- }
52
- getRequest(binId, reqId) {
53
- return this._requests.get(binId)?.find((r) => r.id === reqId);
54
- }
55
- clearRequests(binId) {
56
- const bin = this._bins.get(binId);
57
- if (!bin) return;
58
- this._requests.set(binId, []);
59
- bin.requestCount = 0;
60
- }
61
- cleanupExpired(now) {
62
- const removed = [];
63
- for (const [id, bin] of this._bins) if (Date.parse(bin.expiresAt) <= now) {
64
- this._bins.delete(id);
65
- this._requests.delete(id);
66
- removed.push(id);
67
- }
68
- return removed;
69
- }
70
- };
71
- //#endregion
72
- //#region src/bin-manager.ts
73
- const DEFAULT_TTL_MS = 864e5;
74
- const DEFAULT_MAX_REQUESTS = 100;
75
- const DEFAULT_MAX_BODY_SIZE = 1048576;
76
- const DEFAULT_ID_LENGTH = 12;
77
- const DEFAULT_CLEANUP_INTERVAL_MS = 6e4;
78
- /**
79
- * Manages request bins: ephemeral URL endpoints that capture incoming HTTP
80
- * requests so they can be inspected later.
81
- */
82
- var BinManager = class {
83
- _store;
84
- _defaultTtlMs;
85
- _maxRequestsPerBin;
86
- _maxBodySize;
87
- _idLength;
88
- _cleanupIntervalMs;
89
- _cleanupTimer;
90
- constructor(options) {
91
- this._store = options?.store ?? new InMemoryBinStore();
92
- this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
93
- this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
94
- this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
95
- this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
96
- this._cleanupIntervalMs = options?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
97
- }
98
- /** Underlying storage backend. */
99
- get store() {
100
- return this._store;
101
- }
102
- /** Maximum captured body size in bytes. */
103
- get maxBodySize() {
104
- return this._maxBodySize;
105
- }
106
- /** Maximum number of requests retained per bin. */
107
- get maxRequestsPerBin() {
108
- return this._maxRequestsPerBin;
109
- }
110
- /** Default bin lifetime in milliseconds. */
111
- get defaultTtlMs() {
112
- return this._defaultTtlMs;
113
- }
114
- /** Create a new bin with the configured TTL. */
115
- createBin() {
116
- const id = this.generateId();
117
- const now = Date.now();
118
- const bin = {
119
- id,
120
- createdAt: new Date(now).toISOString(),
121
- expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
122
- requestCount: 0
123
- };
124
- this._store.createBin(bin);
125
- return bin;
126
- }
127
- /**
128
- * Look up a bin by id. Returns `undefined` if the bin does not exist or
129
- * has expired (in which case it is also lazily removed).
130
- */
131
- getBin(id) {
132
- const bin = this._store.getBin(id);
133
- if (!bin) return;
134
- if (Date.parse(bin.expiresAt) <= Date.now()) {
135
- this._store.deleteBin(id);
136
- return;
137
- }
138
- return bin;
139
- }
140
- /** List all non-expired bins. */
141
- listBins() {
142
- const now = Date.now();
143
- const result = [];
144
- for (const bin of this._store.listBins()) {
145
- if (Date.parse(bin.expiresAt) <= now) {
146
- this._store.deleteBin(bin.id);
147
- continue;
148
- }
149
- result.push(bin);
150
- }
151
- return result;
152
- }
153
- /** Delete a bin. Returns true if the bin existed. */
154
- deleteBin(id) {
155
- return this._store.deleteBin(id);
156
- }
157
- /**
158
- * Record an incoming request against a bin. Returns the stored
159
- * {@link CapturedRequest}, or `undefined` if the bin does not exist or is
160
- * expired.
161
- */
162
- recordRequest(binId, raw) {
163
- if (!this.getBin(binId)) return;
164
- const captured = {
165
- ...raw,
166
- id: this.generateId(),
167
- binId,
168
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
169
- };
170
- this._store.addRequest(binId, captured, this._maxRequestsPerBin);
171
- return captured;
172
- }
173
- /** List captured requests for a bin (newest first). */
174
- getRequests(binId) {
175
- return this._store.getRequests(binId);
176
- }
177
- /** Look up a single captured request. */
178
- getRequest(binId, reqId) {
179
- return this._store.getRequest(binId, reqId);
180
- }
181
- /** Clear all captured requests in a bin. */
182
- clearRequests(binId) {
183
- this._store.clearRequests(binId);
184
- }
185
- /** Start the periodic cleanup of expired bins. */
186
- start() {
187
- if (this._cleanupTimer) return;
188
- this._cleanupTimer = setInterval(() => {
189
- this._store.cleanupExpired(Date.now());
190
- }, this._cleanupIntervalMs);
191
- this._cleanupTimer.unref?.();
192
- }
193
- /** Stop the periodic cleanup. Idempotent. */
194
- stop() {
195
- if (this._cleanupTimer) {
196
- clearInterval(this._cleanupTimer);
197
- this._cleanupTimer = void 0;
198
- }
199
- }
200
- generateId() {
201
- return randomUUID().replaceAll("-", "").slice(0, this._idLength);
202
- }
203
- };
204
- //#endregion
205
20
  //#region src/certificate.ts
206
21
  function encodeLength(length) {
207
22
  if (length < 128) return Buffer.from([length]);
@@ -478,6 +293,181 @@ function getFastifyConfig(logging = true) {
478
293
  };
479
294
  }
480
295
  //#endregion
296
+ //#region src/mock-http-bins.ts
297
+ const DEFAULT_TTL_MS = 864e5;
298
+ const DEFAULT_MAX_REQUESTS = 100;
299
+ const DEFAULT_MAX_BODY_SIZE = 131072;
300
+ const DEFAULT_ID_LENGTH = 12;
301
+ const BIN_KEY_PREFIX = "bin:";
302
+ /**
303
+ * Stores request bins in Keyv.
304
+ * The default store is `new Keyv()`, which keeps data in memory.
305
+ */
306
+ var MockHttpBins = class {
307
+ _keyv;
308
+ _defaultTtlMs;
309
+ _maxRequestsPerBin;
310
+ _maxBodySize;
311
+ _idLength;
312
+ _locks = /* @__PURE__ */ new Map();
313
+ constructor(options) {
314
+ this._keyv = options?.keyv ?? new Keyv();
315
+ this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
316
+ this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
317
+ this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
318
+ this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
319
+ }
320
+ /** Keyv instance that stores the bins. */
321
+ get keyv() {
322
+ return this._keyv;
323
+ }
324
+ /** Maximum captured body size in bytes. */
325
+ get maxBodySize() {
326
+ return this._maxBodySize;
327
+ }
328
+ /** Maximum number of requests in one bin. */
329
+ get maxRequestsPerBin() {
330
+ return this._maxRequestsPerBin;
331
+ }
332
+ /** Default bin lifetime in milliseconds. */
333
+ get defaultTtlMs() {
334
+ return this._defaultTtlMs;
335
+ }
336
+ /**
337
+ * Create a new bin with the configured lifetime.
338
+ * Throws when Keyv does not store the bin.
339
+ */
340
+ async createBin() {
341
+ const id = this.generateId();
342
+ const now = Date.now();
343
+ const bin = {
344
+ id,
345
+ createdAt: new Date(now).toISOString(),
346
+ expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
347
+ requestCount: 0
348
+ };
349
+ await this.save({
350
+ bin,
351
+ requests: []
352
+ });
353
+ return bin;
354
+ }
355
+ /**
356
+ * Look up a bin by id.
357
+ * Returns `undefined` when the bin does not exist or Keyv has expired it.
358
+ * A failed read also returns `undefined` when the Keyv instance has an `error` listener.
359
+ */
360
+ async getBin(id) {
361
+ return (await this._keyv.get(this.binKey(id)))?.bin;
362
+ }
363
+ /**
364
+ * List the bins that Keyv still holds.
365
+ * Throws when the Keyv store has no iterator.
366
+ * When the Keyv instance has an `error` listener, a failed walk returns the bins read before the error.
367
+ */
368
+ async listBins() {
369
+ if (!this.canIterate()) throw new Error("MockHttpBins: the configured Keyv store does not support iteration; listBins() is unavailable");
370
+ const bins = [];
371
+ for await (const [key, value] of this._keyv.iterator()) if (key.startsWith(BIN_KEY_PREFIX)) bins.push(value.bin);
372
+ return bins;
373
+ }
374
+ /**
375
+ * Delete a bin. Returns true when the bin existed.
376
+ * The delete waits for captures already queued for that bin, so a capture cannot write the bin back.
377
+ */
378
+ async deleteBin(id) {
379
+ return this.enqueue(id, () => this._keyv.delete(this.binKey(id)));
380
+ }
381
+ /**
382
+ * Record a request for a bin.
383
+ * Returns `undefined` when the bin does not exist.
384
+ * Throws when Keyv does not store the capture.
385
+ */
386
+ async recordRequest(binId, raw) {
387
+ const captured = {
388
+ ...raw,
389
+ id: this.generateId(),
390
+ binId,
391
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
392
+ };
393
+ return await this.enqueue(binId, () => this.appendRequest(binId, captured)) ? captured : void 0;
394
+ }
395
+ /** List captured requests for a bin, newest first. */
396
+ async getRequests(binId) {
397
+ return [...(await this._keyv.get(this.binKey(binId)))?.requests ?? []];
398
+ }
399
+ /** Look up one captured request. */
400
+ async getRequest(binId, reqId) {
401
+ return (await this._keyv.get(this.binKey(binId)))?.requests.find((request) => request.id === reqId);
402
+ }
403
+ /**
404
+ * Remove captured requests and keep the bin.
405
+ * Returns false when the bin does not exist.
406
+ * Throws when Keyv does not store the change.
407
+ */
408
+ async clearRequests(binId) {
409
+ return this.enqueue(binId, async () => {
410
+ const doc = await this._keyv.get(this.binKey(binId));
411
+ if (!doc) return false;
412
+ doc.requests = [];
413
+ doc.bin.requestCount = 0;
414
+ await this.save(doc);
415
+ return true;
416
+ });
417
+ }
418
+ /** Keyv 6 yields nothing when the store has no iterator. Listing must fail instead. */
419
+ canIterate() {
420
+ return typeof this._keyv.store.iterator === "function";
421
+ }
422
+ /** Returns false when the bin does not exist. */
423
+ async appendRequest(binId, captured) {
424
+ const doc = await this._keyv.get(this.binKey(binId));
425
+ if (!doc) return false;
426
+ doc.requests.unshift(captured);
427
+ while (doc.requests.length > this._maxRequestsPerBin) doc.requests.pop();
428
+ doc.bin.requestCount = doc.requests.length;
429
+ await this.save(doc);
430
+ return true;
431
+ }
432
+ /**
433
+ * Write a bin document with a TTL from its lifetime.
434
+ * Keyv returns false or throws when the write fails. Both cases throw here.
435
+ */
436
+ async save(doc) {
437
+ const message = `MockHttpBins: could not save bin ${doc.bin.id}`;
438
+ let saved;
439
+ try {
440
+ saved = await this._keyv.set(this.binKey(doc.bin.id), doc, this.ttlFor(doc.bin));
441
+ } catch (cause) {
442
+ throw new Error(message, { cause });
443
+ }
444
+ if (!saved) throw new Error(message);
445
+ }
446
+ /**
447
+ * Run writes for one bin in order.
448
+ * A failed write does not block the next write for that bin.
449
+ */
450
+ enqueue(id, fn) {
451
+ const run = (this._locks.get(id) ?? Promise.resolve()).then(fn);
452
+ const settled = run.then(() => void 0, () => void 0);
453
+ this._locks.set(id, settled);
454
+ settled.finally(() => {
455
+ if (this._locks.get(id) === settled) this._locks.delete(id);
456
+ });
457
+ return run;
458
+ }
459
+ /** Keyv keeps a key without a TTL forever, so the TTL never drops below 1 ms. */
460
+ ttlFor(bin) {
461
+ return Math.max(1, Date.parse(bin.expiresAt) - Date.now());
462
+ }
463
+ binKey(id) {
464
+ return `${BIN_KEY_PREFIX}${id}`;
465
+ }
466
+ generateId() {
467
+ return randomUUID().replaceAll("-", "").slice(0, this._idLength);
468
+ }
469
+ };
470
+ //#endregion
481
471
  //#region src/routes/anything/index.ts
482
472
  const anythingSchema = {
483
473
  description: "Will return anything that you send it",
@@ -907,9 +897,9 @@ const captureSchema = {
907
897
  const TEXT_CONTENT_TYPE_REGEX = /^(text\/|application\/(json|xml|x-www-form-urlencoded|javascript))/i;
908
898
  /**
909
899
  * Create a Fastify plugin that registers the catch-all bin capture endpoint
910
- * at `/b/:id` and `/b/:id/*`, with the provided {@link BinManager} injected.
900
+ * at `/b/:id` and `/b/:id/*`, with the provided {@link MockHttpBins} injected.
911
901
  */
912
- const binsCaptureRoute = (binManager) => {
902
+ const binsCaptureRoute = (bins) => {
913
903
  return (fastify) => {
914
904
  fastify.removeAllContentTypeParsers();
915
905
  fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
@@ -918,10 +908,9 @@ const binsCaptureRoute = (binManager) => {
918
908
  const handler = async (request, reply) => {
919
909
  const { id } = request.params;
920
910
  const rest = request.params["*"] ?? "";
921
- if (!binManager.getBin(id)) return reply.code(404).send({ error: "bin_not_found" });
922
911
  const raw = request.body;
923
912
  const bodySize = raw?.length ?? 0;
924
- const max = binManager.maxBodySize;
913
+ const max = bins.maxBodySize;
925
914
  const truncated = bodySize > max;
926
915
  const slice = raw && raw.length > 0 ? raw.subarray(0, max) : void 0;
927
916
  const contentTypeHeader = request.headers["content-type"];
@@ -942,7 +931,7 @@ const binsCaptureRoute = (binManager) => {
942
931
  const prefix = `/b/${id}`;
943
932
  const url = request.url.slice(prefix.length) || "/";
944
933
  const path = rest ? `/${rest}` : "/";
945
- const captured = binManager.recordRequest(id, {
934
+ const captured = await bins.recordRequest(id, {
946
935
  method: request.method,
947
936
  url,
948
937
  path,
@@ -955,11 +944,11 @@ const binsCaptureRoute = (binManager) => {
955
944
  bodyEncoding,
956
945
  truncated
957
946
  });
947
+ if (!captured) return reply.code(404).send({ error: "bin_not_found" });
958
948
  return reply.code(200).send({
959
949
  ok: true,
960
950
  binId: id,
961
- /* v8 ignore next -- @preserve */
962
- requestId: captured?.id ?? ""
951
+ requestId: captured.id
963
952
  });
964
953
  };
965
954
  const opts = {
@@ -1174,12 +1163,12 @@ const clearRequestsSchema = {
1174
1163
  };
1175
1164
  /**
1176
1165
  * Create a Fastify plugin that registers bin management endpoints, with the
1177
- * provided {@link BinManager} injected.
1166
+ * provided {@link MockHttpBins} injected.
1178
1167
  */
1179
- const binsManagementRoute = (binManager) => {
1168
+ const binsManagementRoute = (bins) => {
1180
1169
  return (fastify) => {
1181
1170
  fastify.post("/bins", { schema: createBinSchema }, async (request, _reply) => {
1182
- const bin = binManager.createBin();
1171
+ const bin = await bins.createBin();
1183
1172
  /* v8 ignore next -- @preserve */
1184
1173
  const host = request.headers.host ?? request.hostname;
1185
1174
  return {
@@ -1187,29 +1176,28 @@ const binsManagementRoute = (binManager) => {
1187
1176
  url: `${request.protocol}://${host}/b/${bin.id}`
1188
1177
  };
1189
1178
  });
1190
- fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: binManager.listBins() }));
1179
+ fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: await bins.listBins() }));
1191
1180
  fastify.get("/bins/:id", { schema: getBinSchema }, async (request, reply) => {
1192
- const bin = binManager.getBin(request.params.id);
1181
+ const bin = await bins.getBin(request.params.id);
1193
1182
  if (!bin) return reply.code(404).send({ error: "bin_not_found" });
1194
1183
  return bin;
1195
1184
  });
1196
1185
  fastify.get("/bins/:id/requests", { schema: listRequestsSchema }, async (request, reply) => {
1197
- if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1198
- return { requests: binManager.getRequests(request.params.id) };
1186
+ if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1187
+ return { requests: await bins.getRequests(request.params.id) };
1199
1188
  });
1200
1189
  fastify.get("/bins/:id/requests/:reqId", { schema: getRequestSchema }, async (request, reply) => {
1201
- if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1202
- const captured = binManager.getRequest(request.params.id, request.params.reqId);
1190
+ if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1191
+ const captured = await bins.getRequest(request.params.id, request.params.reqId);
1203
1192
  if (!captured) return reply.code(404).send({ error: "request_not_found" });
1204
1193
  return captured;
1205
1194
  });
1206
1195
  fastify.delete("/bins/:id/requests", { schema: clearRequestsSchema }, async (request, reply) => {
1207
- if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1208
- binManager.clearRequests(request.params.id);
1196
+ if (!await bins.clearRequests(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1209
1197
  return reply.code(204).send();
1210
1198
  });
1211
1199
  fastify.delete("/bins/:id", { schema: deleteBinSchema }, async (request, reply) => {
1212
- if (!binManager.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1200
+ if (!await bins.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1213
1201
  return reply.code(204).send();
1214
1202
  });
1215
1203
  };
@@ -3367,7 +3355,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
3367
3355
 
3368
3356
  [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.
3369
3357
  `,
3370
- version: "2.1.0"
3358
+ version: "3.0.0"
3371
3359
  } } };
3372
3360
  const withLocalServer = (spec) => {
3373
3361
  spec.servers = [{
@@ -3502,7 +3490,6 @@ var MockHttp = class extends Hookified {
3502
3490
  _siteDistPath = defaultSiteDistPath;
3503
3491
  _logging = true;
3504
3492
  _pluginTimeout;
3505
- _startBins = true;
3506
3493
  _httpBin = {
3507
3494
  httpMethods: true,
3508
3495
  redirects: true,
@@ -3528,7 +3515,7 @@ var MockHttp = class extends Hookified {
3528
3515
  _httpsCredentials;
3529
3516
  _server;
3530
3517
  _taps = new TapManager();
3531
- _bins = new BinManager();
3518
+ _bins = new MockHttpBins();
3532
3519
  constructor(options) {
3533
3520
  super(options?.hookOptions);
3534
3521
  if (options?.port) this._port = options.port;
@@ -3545,7 +3532,6 @@ var MockHttp = class extends Hookified {
3545
3532
  }
3546
3533
  if (options?.logging !== void 0) this._logging = options.logging;
3547
3534
  if (options?.pluginTimeout !== void 0) this._pluginTimeout = options.pluginTimeout;
3548
- if (options?.startBins !== void 0) this._startBins = options.startBins;
3549
3535
  if (options?.http2 !== void 0) this._http2 = options.http2;
3550
3536
  if (options?.http1 !== void 0) this._http1 = options.http1;
3551
3537
  if (options?.https !== void 0) {
@@ -3679,20 +3665,6 @@ var MockHttp = class extends Hookified {
3679
3665
  this._pluginTimeout = pluginTimeout;
3680
3666
  }
3681
3667
  /**
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
- /**
3696
3668
  * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
3697
3669
  * or provide an HttpsOptions object with cert/key or auto-generation options.
3698
3670
  */
@@ -3798,13 +3770,13 @@ var MockHttp = class extends Hookified {
3798
3770
  this._taps = taps;
3799
3771
  }
3800
3772
  /**
3801
- * The BinManager instance for managing request bins.
3773
+ * The MockHttpBins instance for request bins.
3802
3774
  */
3803
3775
  get bins() {
3804
3776
  return this._bins;
3805
3777
  }
3806
3778
  /**
3807
- * The BinManager instance for managing request bins.
3779
+ * The MockHttpBins instance for request bins.
3808
3780
  */
3809
3781
  set bins(bins) {
3810
3782
  this._bins = bins;
@@ -3874,10 +3846,7 @@ var MockHttp = class extends Hookified {
3874
3846
  if (auth) await this.registerAuthRoutes();
3875
3847
  if (images) await this.registerImageRoutes();
3876
3848
  if (dynamicData) await this.registerDynamicDataRoutes();
3877
- if (bins) {
3878
- await this.registerBinRoutes();
3879
- if (this._startBins) this._bins.start();
3880
- }
3849
+ if (bins) await this.registerBinRoutes();
3881
3850
  if (this._apiDocs) await this.registerSite();
3882
3851
  } catch (error) {
3883
3852
  /* v8 ignore next -- @preserve */
@@ -3908,7 +3877,6 @@ var MockHttp = class extends Hookified {
3908
3877
  * Close the Fastify server.
3909
3878
  */
3910
3879
  async close() {
3911
- this._bins.stop();
3912
3880
  if (this._server) await this._server.close();
3913
3881
  }
3914
3882
  /**
@@ -4111,4 +4079,4 @@ const start = async () => {
4111
4079
  /* v8 ignore next -- @preserve */
4112
4080
  if (import.meta.url === `file://${process$1.argv[1]}`) await start();
4113
4081
  //#endregion
4114
- export { BinManager, InMemoryBinStore, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
4082
+ export { MockHttpBins, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@jaredwray/mockhttp",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Mock Http - Easy way to mock http with httpbin replacement",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
8
8
  "engines": {
9
- "node": ">=22.18.0"
9
+ "node": ">=22.19.0"
10
10
  },
11
11
  "directories": {
12
12
  "test": "test"
@@ -39,29 +39,32 @@
39
39
  },
40
40
  "homepage": "https://github.com/jaredwray/mockhttp#readme",
41
41
  "devDependencies": {
42
- "@biomejs/biome": "^2.5.13",
43
- "@swc/core": "^1.16.2",
42
+ "@biomejs/biome": "^2.5.15",
43
+ "@swc/core": "^1.16.18",
44
44
  "@types/html-escaper": "^3.0.4",
45
- "@vitest/coverage-v8": "^5.0.0",
46
- "docula": "^3.0.0",
45
+ "@vitest/coverage-v8": "^5.0.3",
46
+ "docula": "^3.0.1",
47
47
  "rimraf": "^6.1.3",
48
- "tsdown": "^0.23.0",
49
- "tsx": "^4.23.13",
48
+ "tsdown": "^0.23.1",
49
+ "tsx": "^4.23.15",
50
50
  "typescript": "7.0.2",
51
- "vitest": "^5.0.0"
51
+ "vite": "^8.3.4",
52
+ "vitest": "^5.0.3"
52
53
  },
53
54
  "dependencies": {
54
- "@fastify/cookie": "^11.1.2",
55
- "@fastify/helmet": "^13.1.1",
56
- "@fastify/rate-limit": "^11.2.0",
57
- "@fastify/static": "^10.1.3",
58
- "@fastify/swagger": "^9.8.1",
55
+ "@fastify/cookie": "^11.1.3",
56
+ "@fastify/helmet": "^13.1.2",
57
+ "@fastify/rate-limit": "^11.2.1",
58
+ "@fastify/static": "^10.1.6",
59
+ "@fastify/swagger": "^9.9.2",
60
+ "@keyv/mongo": "^6.1.0",
59
61
  "detect-port": "^2.1.0",
60
- "fastify": "^5.12.4",
61
- "hookified": "^3.0.3",
62
+ "fastify": "^5.12.5",
63
+ "hookified": "^3.0.4",
62
64
  "html-escaper": "^3.0.3",
63
- "pino": "^10.3.1",
64
- "pino-pretty": "^13.1.3"
65
+ "keyv": "^6.1.0",
66
+ "pino": "^10.4.0",
67
+ "pino-pretty": "^13.2.0"
65
68
  },
66
69
  "files": [
67
70
  "dist",
@@ -732,7 +732,7 @@
732
732
  <main class="api-content">
733
733
  <section class="api-info">
734
734
  <h1 class="api-info__title">Mock HTTP API</h1>
735
- <span class="api-info__version">v2.1.0</span>
735
+ <span class="api-info__version">v3.0.0</span>
736
736
  <div class="api-info__description"><p>A simple HTTP server that can be used to mock HTTP responses for testing purposes. Inspired by <a href="https://httpbin.org/">httpbin</a> and built using <code>nodejs</code> and <code>fastify</code> with the idea of running it via <a href="https://mockhttp.org">https://mockhttp.org</a>, via docker <code>jaredwray/mockhttp</code>, or nodejs <code>npm install @jaredwray/mockhttp</code>.</p>
737
737
  <ul>
738
738
  <li><a href="https://github.com/jaredwray/mockhttp">GitHub Repository</a></li>
@@ -15066,7 +15066,7 @@ data &#x3D; response.json()</code></pre>
15066
15066
  &quot;remoteAddress&quot;: &quot;string&quot;,
15067
15067
  &quot;contentType&quot;: &quot;string&quot;,
15068
15068
  &quot;bodySize&quot;: 0,
15069
- &quot;body&quot;: null,
15069
+ &quot;body&quot;: &quot;string&quot;,
15070
15070
  &quot;bodyEncoding&quot;: &quot;utf8&quot;,
15071
15071
  &quot;truncated&quot;: true,
15072
15072
  &quot;capturedAt&quot;: &quot;string&quot;
@@ -15409,7 +15409,7 @@ data &#x3D; response.json()</code></pre>
15409
15409
  </div>
15410
15410
  <div class="api-schema-prop">
15411
15411
  <span class="api-schema-prop-name">body</span>
15412
- <span class="api-schema-prop-type">null,string</span>
15412
+ <span class="api-schema-prop-type">string</span>
15413
15413
  <span class="api-schema-prop-desc"></span>
15414
15414
  </div>
15415
15415
  <div class="api-schema-prop">
@@ -15445,7 +15445,7 @@ data &#x3D; response.json()</code></pre>
15445
15445
  &quot;remoteAddress&quot;: &quot;string&quot;,
15446
15446
  &quot;contentType&quot;: &quot;string&quot;,
15447
15447
  &quot;bodySize&quot;: 0,
15448
- &quot;body&quot;: null,
15448
+ &quot;body&quot;: &quot;string&quot;,
15449
15449
  &quot;bodyEncoding&quot;: &quot;utf8&quot;,
15450
15450
  &quot;truncated&quot;: true,
15451
15451
  &quot;capturedAt&quot;: &quot;string&quot;
@@ -17716,7 +17716,7 @@ data &#x3D; response.json()</code></pre>
17716
17716
  <a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
17717
17717
  <svg class="footer__bat" xmlns="http://www.w3.org/2000/svg" viewBox="70 110 360 205" fill="currentColor"><path d="M420,173c-16.76-18-72.78-36.65-131.59-30.58A160.15,160.15,0,0,0,269,120.55c-3.78,4.52-11.15,9.45-19,9.45s-15.23-4.93-19-9.45a160.15,160.15,0,0,0-19.4,21.83C152.78,136.31,96.76,154.94,80,173c48.86,5.86,79.5,34.09,79.5,72.06,52.37,0,77,28,90.5,59.94C263.54,273,288.13,245,340.5,245,340.5,207,371.14,178.8,420,173Z"/><circle cx="239.19" cy="145.36" r="3.32" fill="#fff"/><circle cx="260.81" cy="145.36" r="3.32" fill="#fff"/></svg>
17718
17718
  </a>
17719
- <span class="footer__updated">updated: 2026-10-03</span>
17719
+ <span class="footer__updated">updated: 2026-10-20</span>
17720
17720
  </footer>
17721
17721
  <script src="/css/highlight/highlight.min.js"></script>
17722
17722
  <script>