@jaredwray/mockhttp 1.6.1 → 1.8.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
@@ -16,6 +16,192 @@ import { readFileSync } from "node:fs";
16
16
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
17
17
  import { escape } from "html-escaper";
18
18
  import { fastifySwaggerUi } from "@fastify/swagger-ui";
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 = 1440 * 60 * 1e3;
74
+ const DEFAULT_MAX_REQUESTS = 100;
75
+ const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
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
19
205
  //#region src/certificate.ts
20
206
  function encodeLength(length) {
21
207
  if (length < 128) return Buffer.from([length]);
@@ -249,7 +435,9 @@ async function generateCertificateFiles(options) {
249
435
  function isSpecificMatch(route) {
250
436
  if (!route) return false;
251
437
  const params = route.params;
252
- return params === void 0 || params["*"] === void 0;
438
+ if (!params) return true;
439
+ const keys = Object.keys(params);
440
+ return keys.length === 0 || keys.some((k) => k !== "*");
253
441
  }
254
442
  function rewriteUrl(req) {
255
443
  const originalUrl = req.url ?? "/";
@@ -452,7 +640,7 @@ const basicAuthRoute = (fastify) => {
452
640
  const parseBearer = (header) => {
453
641
  if (!header) return;
454
642
  const [scheme, token] = header.split(" ");
455
- if (!scheme || scheme.toLowerCase() !== "bearer" || !token) return;
643
+ if (scheme?.toLowerCase() !== "bearer" || !token) return;
456
644
  return token;
457
645
  };
458
646
  const bearerAuthSchema = {
@@ -526,7 +714,7 @@ const parseDigest = (header) => {
526
714
  if (spaceIndex === -1) return;
527
715
  const scheme = header.slice(0, spaceIndex);
528
716
  const rest = header.slice(spaceIndex + 1);
529
- if (!scheme || scheme.toLowerCase() !== "digest" || !rest) return;
717
+ if (scheme?.toLowerCase() !== "digest" || !rest) return;
530
718
  const out = {};
531
719
  const parts = [];
532
720
  let current = "";
@@ -634,7 +822,7 @@ const digestAuthRoute = (fastify) => {
634
822
  const parseBasic = (header) => {
635
823
  if (!header) return;
636
824
  const [scheme, value] = header.split(" ");
637
- if (!scheme || scheme.toLowerCase() !== "basic" || !value) return;
825
+ if (scheme?.toLowerCase() !== "basic" || !value) return;
638
826
  try {
639
827
  const decoded = Buffer$1.from(value, "base64").toString("utf8");
640
828
  const idx = decoded.indexOf(":");
@@ -676,6 +864,356 @@ const hiddenBasicAuthRoute = (fastify) => {
676
864
  });
677
865
  };
678
866
  //#endregion
867
+ //#region src/routes/bins/capture.ts
868
+ const captureSchema = {
869
+ description: "Capture any HTTP request sent to a bin. Stores method, headers, body, query, and metadata.",
870
+ tags: ["Bins"],
871
+ params: {
872
+ type: "object",
873
+ properties: {
874
+ id: {
875
+ type: "string",
876
+ description: "Bin identifier"
877
+ },
878
+ "*": {
879
+ type: "string",
880
+ description: "Sub-path captured with the request"
881
+ }
882
+ },
883
+ required: ["id"]
884
+ },
885
+ response: {
886
+ 200: {
887
+ type: "object",
888
+ properties: {
889
+ ok: { type: "boolean" },
890
+ binId: { type: "string" },
891
+ requestId: { type: "string" }
892
+ },
893
+ required: [
894
+ "ok",
895
+ "binId",
896
+ "requestId"
897
+ ]
898
+ },
899
+ 404: {
900
+ type: "object",
901
+ properties: { error: { type: "string" } },
902
+ required: ["error"]
903
+ }
904
+ }
905
+ };
906
+ const TEXT_CONTENT_TYPE_REGEX = /^(text\/|application\/(json|xml|x-www-form-urlencoded|javascript))/i;
907
+ /**
908
+ * Create a Fastify plugin that registers the catch-all bin capture endpoint
909
+ * at `/b/:id` and `/b/:id/*`, with the provided {@link BinManager} injected.
910
+ */
911
+ const binsCaptureRoute = (binManager) => {
912
+ return (fastify) => {
913
+ fastify.removeAllContentTypeParsers();
914
+ fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
915
+ done(null, body);
916
+ });
917
+ const handler = async (request, reply) => {
918
+ const { id } = request.params;
919
+ const rest = request.params["*"] ?? "";
920
+ if (!binManager.getBin(id)) return reply.code(404).send({ error: "bin_not_found" });
921
+ const raw = request.body;
922
+ const bodySize = raw?.length ?? 0;
923
+ const max = binManager.maxBodySize;
924
+ const truncated = bodySize > max;
925
+ const slice = raw && raw.length > 0 ? raw.subarray(0, max) : void 0;
926
+ const contentTypeHeader = request.headers["content-type"];
927
+ const contentType = typeof contentTypeHeader === "string" ? contentTypeHeader : void 0;
928
+ const isText = contentType ? TEXT_CONTENT_TYPE_REGEX.test(contentType) : false;
929
+ let bodyEncoding;
930
+ let body;
931
+ if (!slice) {
932
+ bodyEncoding = "none";
933
+ body = null;
934
+ } else if (isText) {
935
+ bodyEncoding = "utf8";
936
+ body = slice.toString("utf8");
937
+ } else {
938
+ bodyEncoding = "base64";
939
+ body = slice.toString("base64");
940
+ }
941
+ const prefix = `/b/${id}`;
942
+ const url = request.url.slice(prefix.length) || "/";
943
+ const path = rest ? `/${rest}` : "/";
944
+ const captured = binManager.recordRequest(id, {
945
+ method: request.method,
946
+ url,
947
+ path,
948
+ query: request.query,
949
+ headers: request.headers,
950
+ remoteAddress: request.ip,
951
+ contentType,
952
+ bodySize,
953
+ body,
954
+ bodyEncoding,
955
+ truncated
956
+ });
957
+ return reply.code(200).send({
958
+ ok: true,
959
+ binId: id,
960
+ /* v8 ignore next -- @preserve */
961
+ requestId: captured?.id ?? ""
962
+ });
963
+ };
964
+ const opts = {
965
+ schema: captureSchema,
966
+ /* v8 ignore next -- @preserve */
967
+ validatorCompiler: () => () => true
968
+ };
969
+ for (const m of [
970
+ "get",
971
+ "post",
972
+ "put",
973
+ "patch",
974
+ "delete",
975
+ "options"
976
+ ]) {
977
+ fastify[m]("/b/:id", opts, handler);
978
+ fastify[m]("/b/:id/*", opts, handler);
979
+ }
980
+ };
981
+ };
982
+ //#endregion
983
+ //#region src/routes/bins/management.ts
984
+ const binSchema = {
985
+ type: "object",
986
+ properties: {
987
+ id: { type: "string" },
988
+ createdAt: { type: "string" },
989
+ expiresAt: { type: "string" },
990
+ requestCount: { type: "integer" }
991
+ },
992
+ required: [
993
+ "id",
994
+ "createdAt",
995
+ "expiresAt",
996
+ "requestCount"
997
+ ]
998
+ };
999
+ const capturedRequestSchema = {
1000
+ type: "object",
1001
+ properties: {
1002
+ id: { type: "string" },
1003
+ binId: { type: "string" },
1004
+ method: { type: "string" },
1005
+ url: { type: "string" },
1006
+ path: { type: "string" },
1007
+ query: {
1008
+ type: "object",
1009
+ additionalProperties: true
1010
+ },
1011
+ headers: {
1012
+ type: "object",
1013
+ additionalProperties: true
1014
+ },
1015
+ remoteAddress: { type: "string" },
1016
+ contentType: { type: "string" },
1017
+ bodySize: { type: "integer" },
1018
+ body: { type: ["string", "null"] },
1019
+ bodyEncoding: {
1020
+ type: "string",
1021
+ enum: [
1022
+ "utf8",
1023
+ "base64",
1024
+ "none"
1025
+ ]
1026
+ },
1027
+ truncated: { type: "boolean" },
1028
+ capturedAt: { type: "string" }
1029
+ },
1030
+ required: [
1031
+ "id",
1032
+ "binId",
1033
+ "method",
1034
+ "url",
1035
+ "path",
1036
+ "query",
1037
+ "headers",
1038
+ "remoteAddress",
1039
+ "bodySize",
1040
+ "body",
1041
+ "bodyEncoding",
1042
+ "truncated",
1043
+ "capturedAt"
1044
+ ]
1045
+ };
1046
+ const createBinSchema = {
1047
+ description: "Create a new request bin",
1048
+ tags: ["Bins"],
1049
+ response: { 200: {
1050
+ type: "object",
1051
+ properties: {
1052
+ id: { type: "string" },
1053
+ url: { type: "string" },
1054
+ createdAt: { type: "string" },
1055
+ expiresAt: { type: "string" },
1056
+ requestCount: { type: "integer" }
1057
+ },
1058
+ required: [
1059
+ "id",
1060
+ "url",
1061
+ "createdAt",
1062
+ "expiresAt",
1063
+ "requestCount"
1064
+ ]
1065
+ } }
1066
+ };
1067
+ const listBinsSchema = {
1068
+ description: "List all active bins",
1069
+ tags: ["Bins"],
1070
+ response: { 200: {
1071
+ type: "object",
1072
+ properties: { bins: {
1073
+ type: "array",
1074
+ items: binSchema
1075
+ } },
1076
+ required: ["bins"]
1077
+ } }
1078
+ };
1079
+ const getBinSchema = {
1080
+ description: "Get metadata for a single bin",
1081
+ tags: ["Bins"],
1082
+ params: {
1083
+ type: "object",
1084
+ properties: { id: { type: "string" } },
1085
+ required: ["id"]
1086
+ },
1087
+ response: {
1088
+ 200: binSchema,
1089
+ 404: {
1090
+ type: "object",
1091
+ properties: { error: { type: "string" } },
1092
+ required: ["error"]
1093
+ }
1094
+ }
1095
+ };
1096
+ const listRequestsSchema = {
1097
+ description: "List captured requests for a bin (newest first)",
1098
+ tags: ["Bins"],
1099
+ params: {
1100
+ type: "object",
1101
+ properties: { id: { type: "string" } },
1102
+ required: ["id"]
1103
+ },
1104
+ response: {
1105
+ 200: {
1106
+ type: "object",
1107
+ properties: { requests: {
1108
+ type: "array",
1109
+ items: capturedRequestSchema
1110
+ } },
1111
+ required: ["requests"]
1112
+ },
1113
+ 404: {
1114
+ type: "object",
1115
+ properties: { error: { type: "string" } },
1116
+ required: ["error"]
1117
+ }
1118
+ }
1119
+ };
1120
+ const getRequestSchema = {
1121
+ description: "Get a single captured request by id",
1122
+ tags: ["Bins"],
1123
+ params: {
1124
+ type: "object",
1125
+ properties: {
1126
+ id: { type: "string" },
1127
+ reqId: { type: "string" }
1128
+ },
1129
+ required: ["id", "reqId"]
1130
+ },
1131
+ response: {
1132
+ 200: capturedRequestSchema,
1133
+ 404: {
1134
+ type: "object",
1135
+ properties: { error: { type: "string" } },
1136
+ required: ["error"]
1137
+ }
1138
+ }
1139
+ };
1140
+ const deleteBinSchema = {
1141
+ description: "Delete a bin and all its captured requests",
1142
+ tags: ["Bins"],
1143
+ params: {
1144
+ type: "object",
1145
+ properties: { id: { type: "string" } },
1146
+ required: ["id"]
1147
+ },
1148
+ response: {
1149
+ 204: { type: "null" },
1150
+ 404: {
1151
+ type: "object",
1152
+ properties: { error: { type: "string" } },
1153
+ required: ["error"]
1154
+ }
1155
+ }
1156
+ };
1157
+ const clearRequestsSchema = {
1158
+ description: "Clear all captured requests for a bin",
1159
+ tags: ["Bins"],
1160
+ params: {
1161
+ type: "object",
1162
+ properties: { id: { type: "string" } },
1163
+ required: ["id"]
1164
+ },
1165
+ response: {
1166
+ 204: { type: "null" },
1167
+ 404: {
1168
+ type: "object",
1169
+ properties: { error: { type: "string" } },
1170
+ required: ["error"]
1171
+ }
1172
+ }
1173
+ };
1174
+ /**
1175
+ * Create a Fastify plugin that registers bin management endpoints, with the
1176
+ * provided {@link BinManager} injected.
1177
+ */
1178
+ const binsManagementRoute = (binManager) => {
1179
+ return (fastify) => {
1180
+ fastify.post("/bins", { schema: createBinSchema }, async (request, _reply) => {
1181
+ const bin = binManager.createBin();
1182
+ /* v8 ignore next -- @preserve */
1183
+ const host = request.headers.host ?? request.hostname;
1184
+ return {
1185
+ ...bin,
1186
+ url: `${request.protocol}://${host}/b/${bin.id}`
1187
+ };
1188
+ });
1189
+ fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: binManager.listBins() }));
1190
+ fastify.get("/bins/:id", { schema: getBinSchema }, async (request, reply) => {
1191
+ const bin = binManager.getBin(request.params.id);
1192
+ if (!bin) return reply.code(404).send({ error: "bin_not_found" });
1193
+ return bin;
1194
+ });
1195
+ fastify.get("/bins/:id/requests", { schema: listRequestsSchema }, async (request, reply) => {
1196
+ if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1197
+ return { requests: binManager.getRequests(request.params.id) };
1198
+ });
1199
+ fastify.get("/bins/:id/requests/:reqId", { schema: getRequestSchema }, async (request, reply) => {
1200
+ if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1201
+ const captured = binManager.getRequest(request.params.id, request.params.reqId);
1202
+ if (!captured) return reply.code(404).send({ error: "request_not_found" });
1203
+ return captured;
1204
+ });
1205
+ fastify.delete("/bins/:id/requests", { schema: clearRequestsSchema }, async (request, reply) => {
1206
+ if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1207
+ binManager.clearRequests(request.params.id);
1208
+ return reply.code(204).send();
1209
+ });
1210
+ fastify.delete("/bins/:id", { schema: deleteBinSchema }, async (request, reply) => {
1211
+ if (!binManager.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
1212
+ return reply.code(204).send();
1213
+ });
1214
+ };
1215
+ };
1216
+ //#endregion
679
1217
  //#region src/routes/cookies/delete.ts
680
1218
  const cookiesDeleteSchema = {
681
1219
  description: "Delete a cookie",
@@ -2911,7 +3449,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
2911
3449
 
2912
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.
2913
3451
  `,
2914
- version: "1.6.1"
3452
+ version: "1.8.0"
2915
3453
  },
2916
3454
  consumes: ["application/json"],
2917
3455
  produces: ["application/json"]
@@ -3058,7 +3596,8 @@ var MockHttp = class extends Hookified {
3058
3596
  anything: true,
3059
3597
  auth: true,
3060
3598
  images: true,
3061
- dynamicData: true
3599
+ dynamicData: true,
3600
+ bins: true
3062
3601
  };
3063
3602
  _rateLimit = {
3064
3603
  max: 1e3,
@@ -3071,6 +3610,7 @@ var MockHttp = class extends Hookified {
3071
3610
  _httpsCredentials;
3072
3611
  _server = Fastify();
3073
3612
  _taps = new TapManager();
3613
+ _bins = new BinManager();
3074
3614
  constructor(options) {
3075
3615
  super(options?.hookOptions);
3076
3616
  if (options?.port) this._port = options.port;
@@ -3276,6 +3816,18 @@ var MockHttp = class extends Hookified {
3276
3816
  this._taps = taps;
3277
3817
  }
3278
3818
  /**
3819
+ * The BinManager instance for managing request bins.
3820
+ */
3821
+ get bins() {
3822
+ return this._bins;
3823
+ }
3824
+ /**
3825
+ * The BinManager instance for managing request bins.
3826
+ */
3827
+ set bins(bins) {
3828
+ this._bins = bins;
3829
+ }
3830
+ /**
3279
3831
  * Start the Fastify server. If the server is already running, it will be closed and restarted.
3280
3832
  */
3281
3833
  async start() {
@@ -3334,7 +3886,7 @@ var MockHttp = class extends Hookified {
3334
3886
  } } });
3335
3887
  if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
3336
3888
  if (this._apiDocs) await this.registerApiDocs();
3337
- const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData } = this._httpBin;
3889
+ const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData, bins } = this._httpBin;
3338
3890
  if (httpMethods) await this.registerHttpMethods();
3339
3891
  if (statusCodes) await this.registerStatusCodeRoutes();
3340
3892
  if (requestInspection) await this.registerRequestInspectionRoutes();
@@ -3346,6 +3898,10 @@ var MockHttp = class extends Hookified {
3346
3898
  if (auth) await this.registerAuthRoutes();
3347
3899
  if (images) await this.registerImageRoutes();
3348
3900
  if (dynamicData) await this.registerDynamicDataRoutes();
3901
+ if (bins) {
3902
+ await this.registerBinRoutes();
3903
+ this._bins.start();
3904
+ }
3349
3905
  if (this._autoDetectPort) {
3350
3906
  const originalPort = this._port;
3351
3907
  this._port = await this.detectPort();
@@ -3364,6 +3920,7 @@ var MockHttp = class extends Hookified {
3364
3920
  * Close the Fastify server.
3365
3921
  */
3366
3922
  async close() {
3923
+ this._bins.stop();
3367
3924
  await this._server.close();
3368
3925
  }
3369
3926
  /**
@@ -3488,6 +4045,15 @@ var MockHttp = class extends Hookified {
3488
4045
  await fastify.register(dripRoute);
3489
4046
  await fastify.register(linksRoute);
3490
4047
  }
4048
+ /**
4049
+ * Register the request bin routes (management at /bins and capture at /b/:id).
4050
+ * @param fastifyInstance - the server instance to register the routes on.
4051
+ */
4052
+ async registerBinRoutes(fastifyInstance) {
4053
+ const fastify = fastifyInstance ?? this._server;
4054
+ await fastify.register(binsManagementRoute(this._bins));
4055
+ await fastify.register(binsCaptureRoute(this._bins));
4056
+ }
3491
4057
  async resolveHttpsCredentials(options) {
3492
4058
  if (options.cert && options.key) return {
3493
4059
  cert: await this.loadPemValue(options.cert),
@@ -3526,4 +4092,4 @@ const start = async () => {
3526
4092
  /* v8 ignore next -- @preserve */
3527
4093
  if (import.meta.url === `file://${process$1.argv[1]}`) await start();
3528
4094
  //#endregion
3529
- export { MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
4095
+ export { BinManager, InMemoryBinStore, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };