@jaredwray/mockhttp 1.6.0 → 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 ?? "/";
@@ -359,6 +547,7 @@ const anythingRoute = (fastify) => {
359
547
  };
360
548
  const noValidation = {
361
549
  schema: anythingSchema,
550
+ /* v8 ignore next -- @preserve */
362
551
  validatorCompiler: () => () => true
363
552
  };
364
553
  fastify.get("/anything", noValidation, handler);
@@ -451,7 +640,7 @@ const basicAuthRoute = (fastify) => {
451
640
  const parseBearer = (header) => {
452
641
  if (!header) return;
453
642
  const [scheme, token] = header.split(" ");
454
- if (!scheme || scheme.toLowerCase() !== "bearer" || !token) return;
643
+ if (scheme?.toLowerCase() !== "bearer" || !token) return;
455
644
  return token;
456
645
  };
457
646
  const bearerAuthSchema = {
@@ -525,7 +714,7 @@ const parseDigest = (header) => {
525
714
  if (spaceIndex === -1) return;
526
715
  const scheme = header.slice(0, spaceIndex);
527
716
  const rest = header.slice(spaceIndex + 1);
528
- if (!scheme || scheme.toLowerCase() !== "digest" || !rest) return;
717
+ if (scheme?.toLowerCase() !== "digest" || !rest) return;
529
718
  const out = {};
530
719
  const parts = [];
531
720
  let current = "";
@@ -633,7 +822,7 @@ const digestAuthRoute = (fastify) => {
633
822
  const parseBasic = (header) => {
634
823
  if (!header) return;
635
824
  const [scheme, value] = header.split(" ");
636
- if (!scheme || scheme.toLowerCase() !== "basic" || !value) return;
825
+ if (scheme?.toLowerCase() !== "basic" || !value) return;
637
826
  try {
638
827
  const decoded = Buffer$1.from(value, "base64").toString("utf8");
639
828
  const idx = decoded.indexOf(":");
@@ -675,6 +864,356 @@ const hiddenBasicAuthRoute = (fastify) => {
675
864
  });
676
865
  };
677
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
678
1217
  //#region src/routes/cookies/delete.ts
679
1218
  const cookiesDeleteSchema = {
680
1219
  description: "Delete a cookie",
@@ -900,6 +1439,7 @@ const delayRoute = (fastify) => {
900
1439
  /* v8 ignore next -- @preserve */
901
1440
  const host = request.headers.host || "localhost";
902
1441
  return {
1442
+ /* v8 ignore next -- @preserve */
903
1443
  args: request.query || {},
904
1444
  data: "",
905
1445
  files: {},
@@ -1207,6 +1747,7 @@ const streamRoute = (fastify) => {
1207
1747
  for (let i = 0; i < lines; i++) {
1208
1748
  const data = {
1209
1749
  id: i,
1750
+ /* v8 ignore next -- @preserve */
1210
1751
  args: request.query || {},
1211
1752
  headers: request.headers,
1212
1753
  origin: request.ip,
@@ -2908,7 +3449,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
2908
3449
 
2909
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.
2910
3451
  `,
2911
- version: "1.6.0"
3452
+ version: "1.8.0"
2912
3453
  },
2913
3454
  consumes: ["application/json"],
2914
3455
  produces: ["application/json"]
@@ -2921,14 +3462,17 @@ const registerSwaggerUi = async (fastify) => {
2921
3462
  deepLinking: false
2922
3463
  },
2923
3464
  uiHooks: {
3465
+ /* v8 ignore next -- @preserve */
2924
3466
  onRequest(_request, _reply, next) {
2925
3467
  next();
2926
3468
  },
3469
+ /* v8 ignore next -- @preserve */
2927
3470
  preHandler(_request, _reply, next) {
2928
3471
  next();
2929
3472
  }
2930
3473
  },
2931
3474
  staticCSP: true,
3475
+ /* v8 ignore next -- @preserve */
2932
3476
  transformSpecification: (swaggerObject, _request, _reply) => swaggerObject,
2933
3477
  transformSpecificationClone: true
2934
3478
  });
@@ -3052,7 +3596,8 @@ var MockHttp = class extends Hookified {
3052
3596
  anything: true,
3053
3597
  auth: true,
3054
3598
  images: true,
3055
- dynamicData: true
3599
+ dynamicData: true,
3600
+ bins: true
3056
3601
  };
3057
3602
  _rateLimit = {
3058
3603
  max: 1e3,
@@ -3065,6 +3610,7 @@ var MockHttp = class extends Hookified {
3065
3610
  _httpsCredentials;
3066
3611
  _server = Fastify();
3067
3612
  _taps = new TapManager();
3613
+ _bins = new BinManager();
3068
3614
  constructor(options) {
3069
3615
  super(options?.hookOptions);
3070
3616
  if (options?.port) this._port = options.port;
@@ -3270,6 +3816,18 @@ var MockHttp = class extends Hookified {
3270
3816
  this._taps = taps;
3271
3817
  }
3272
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
+ /**
3273
3831
  * Start the Fastify server. If the server is already running, it will be closed and restarted.
3274
3832
  */
3275
3833
  async start() {
@@ -3328,7 +3886,7 @@ var MockHttp = class extends Hookified {
3328
3886
  } } });
3329
3887
  if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
3330
3888
  if (this._apiDocs) await this.registerApiDocs();
3331
- 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;
3332
3890
  if (httpMethods) await this.registerHttpMethods();
3333
3891
  if (statusCodes) await this.registerStatusCodeRoutes();
3334
3892
  if (requestInspection) await this.registerRequestInspectionRoutes();
@@ -3340,6 +3898,10 @@ var MockHttp = class extends Hookified {
3340
3898
  if (auth) await this.registerAuthRoutes();
3341
3899
  if (images) await this.registerImageRoutes();
3342
3900
  if (dynamicData) await this.registerDynamicDataRoutes();
3901
+ if (bins) {
3902
+ await this.registerBinRoutes();
3903
+ this._bins.start();
3904
+ }
3343
3905
  if (this._autoDetectPort) {
3344
3906
  const originalPort = this._port;
3345
3907
  this._port = await this.detectPort();
@@ -3358,6 +3920,7 @@ var MockHttp = class extends Hookified {
3358
3920
  * Close the Fastify server.
3359
3921
  */
3360
3922
  async close() {
3923
+ this._bins.stop();
3361
3924
  await this._server.close();
3362
3925
  }
3363
3926
  /**
@@ -3482,6 +4045,15 @@ var MockHttp = class extends Hookified {
3482
4045
  await fastify.register(dripRoute);
3483
4046
  await fastify.register(linksRoute);
3484
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
+ }
3485
4057
  async resolveHttpsCredentials(options) {
3486
4058
  if (options.cert && options.key) return {
3487
4059
  cert: await this.loadPemValue(options.cert),
@@ -3520,4 +4092,4 @@ const start = async () => {
3520
4092
  /* v8 ignore next -- @preserve */
3521
4093
  if (import.meta.url === `file://${process$1.argv[1]}`) await start();
3522
4094
  //#endregion
3523
- export { MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
4095
+ export { BinManager, InMemoryBinStore, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };