@jaredwray/mockhttp 1.6.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
13
13
  # Features
14
14
  * All the features of [httpbin](https://httpbin.org/)
15
15
  * Taps - Inject custom responses for testing and development
16
+ * Bins - Capture and inspect incoming HTTP requests (great for webhook debugging)
16
17
  * `@fastify/helmet` built in by default
17
18
  * Built with `nodejs`, `typescript`, and `fastify`
18
19
  * Deploy via `docker` or `nodejs`
@@ -28,6 +29,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
28
29
  - [HTTPS Support](#https-support)
29
30
  - [HTTP/2 Support](#http2-support)
30
31
  - [Response Injection (Tap Feature)](#response-injection-tap-feature)
32
+ - [Request Bins](#request-bins)
31
33
  - [Rate Limiting](#rate-limiting)
32
34
  - [Logging](#logging)
33
35
  - [Flexible URL Matching](#flexible-url-matching)
@@ -431,6 +433,362 @@ const tap = mock.taps.inject(
431
433
  );
432
434
  ```
433
435
 
436
+ # Request Bins
437
+
438
+ Bins are ephemeral URL endpoints that **capture incoming HTTP requests** so you
439
+ can inspect them later. They're the inverse of Taps: instead of injecting a
440
+ response, they record everything they receive. Useful for:
441
+
442
+ - **Debugging webhooks** — point Stripe / GitHub / Slack at a bin URL and see
443
+ exactly what they send
444
+ - **Exercising client SDKs** — verify your SDK sends the request shape you expect
445
+ - **Recording fixtures** — capture real traffic to replay in tests later
446
+
447
+ There are two URL prefixes:
448
+
449
+ - **`/bins`** — JSON management API: create, list, inspect, delete bins
450
+ - **`/b/:id`** — the capture URL. Any HTTP method, any sub-path, any body sent
451
+ here is stored against bin `:id`
452
+
453
+ ## Quick Start
454
+
455
+ ```bash
456
+ # 1. create a bin
457
+ curl -s -X POST http://localhost:3000/bins
458
+ # → { "id": "abc123def456", "url": "http://localhost:3000/b/abc123def456",
459
+ # "createdAt": "2026-05-18T12:00:00.000Z",
460
+ # "expiresAt": "2026-05-19T12:00:00.000Z",
461
+ # "requestCount": 0 }
462
+
463
+ # 2. send anything to the capture URL
464
+ curl -X POST "http://localhost:3000/b/abc123def456/webhook?source=stripe" \
465
+ -H 'content-type: application/json' \
466
+ -d '{"event":"payment.succeeded"}'
467
+ # → { "ok": true, "binId": "abc123def456", "requestId": "f9c2e1a40b3d" }
468
+
469
+ # 3. list captured requests (newest first)
470
+ curl http://localhost:3000/bins/abc123def456/requests
471
+ ```
472
+
473
+ ## Endpoints
474
+
475
+ | Method | Path | Description |
476
+ |--------|------|-------------|
477
+ | POST | `/bins` | Create a new bin |
478
+ | GET | `/bins` | List all active bins |
479
+ | GET | `/bins/:id` | Get bin metadata |
480
+ | GET | `/bins/:id/requests` | List captured requests (newest first) |
481
+ | GET | `/bins/:id/requests/:reqId` | Get a single captured request |
482
+ | DELETE | `/bins/:id/requests` | Clear all captured requests in a bin |
483
+ | DELETE | `/bins/:id` | Delete a bin |
484
+ | ANY | `/b/:id` and `/b/:id/*` | Capture requests sent to a bin |
485
+
486
+ ## End-to-End Webhook Debugging Example
487
+
488
+ A complete walkthrough using curl:
489
+
490
+ ```bash
491
+ BASE=http://localhost:3000
492
+
493
+ # Create the bin and grab its id
494
+ ID=$(curl -s -X POST $BASE/bins | jq -r .id)
495
+ echo "Bin URL: $BASE/b/$ID"
496
+
497
+ # Simulate a Stripe webhook
498
+ curl -s -X POST "$BASE/b/$ID/stripe/events?secret=whsec_test" \
499
+ -H 'content-type: application/json' \
500
+ -H 'stripe-signature: t=1700000000,v1=abc123' \
501
+ -d '{"id":"evt_1","type":"payment_intent.succeeded","data":{"object":{"amount":2000}}}'
502
+
503
+ # Simulate a GitHub webhook
504
+ curl -s -X POST "$BASE/b/$ID/github" \
505
+ -H 'content-type: application/json' \
506
+ -H 'x-github-event: pull_request' \
507
+ -d '{"action":"opened","number":42}'
508
+
509
+ # Inspect everything that arrived
510
+ curl -s "$BASE/bins/$ID/requests" | jq
511
+
512
+ # Drill into the most recent one
513
+ RID=$(curl -s "$BASE/bins/$ID/requests" | jq -r '.requests[0].id')
514
+ curl -s "$BASE/bins/$ID/requests/$RID" | jq
515
+ # {
516
+ # "id": "f9c2e1a40b3d",
517
+ # "binId": "abc123def456",
518
+ # "method": "POST",
519
+ # "url": "/github",
520
+ # "path": "/github",
521
+ # "query": {},
522
+ # "headers": {
523
+ # "host": "localhost:3000",
524
+ # "content-type": "application/json",
525
+ # "x-github-event": "pull_request"
526
+ # },
527
+ # "remoteAddress": "127.0.0.1",
528
+ # "contentType": "application/json",
529
+ # "bodySize": 32,
530
+ # "body": "{\"action\":\"opened\",\"number\":42}",
531
+ # "bodyEncoding": "utf8",
532
+ # "truncated": false,
533
+ # "capturedAt": "2026-05-18T12:00:01.234Z"
534
+ # }
535
+
536
+ # Clean up
537
+ curl -s -X DELETE "$BASE/bins/$ID/requests" # clear captures, keep the bin
538
+ curl -s -X DELETE "$BASE/bins/$ID" # delete the bin entirely
539
+ ```
540
+
541
+ ## Captured Request Shape
542
+
543
+ ```json
544
+ {
545
+ "id": "f9c2e1a40b3d",
546
+ "binId": "abc123def456",
547
+ "method": "POST",
548
+ "url": "/webhook?source=stripe",
549
+ "path": "/webhook",
550
+ "query": { "source": "stripe" },
551
+ "headers": { "content-type": "application/json", "...": "..." },
552
+ "remoteAddress": "127.0.0.1",
553
+ "contentType": "application/json",
554
+ "bodySize": 32,
555
+ "body": "{\"event\":\"payment.succeeded\"}",
556
+ "bodyEncoding": "utf8",
557
+ "truncated": false,
558
+ "capturedAt": "2026-05-18T12:00:00.000Z"
559
+ }
560
+ ```
561
+
562
+ - **`url`** is the request URL relative to the bin (everything after `/b/:id`).
563
+ - **`path`** is the sub-path only, without the query string.
564
+ - **`bodyEncoding`** is `"utf8"` for text content types (text/*, application/json,
565
+ application/xml, application/x-www-form-urlencoded, application/javascript),
566
+ `"base64"` for everything else (binary payloads), or `"none"` when no body
567
+ was sent.
568
+ - **`truncated`** is `true` when the body exceeded `maxBodySize` and was cut
569
+ off. `bodySize` reflects the original length.
570
+
571
+ ## Body Encoding Examples
572
+
573
+ ### Text Content (UTF-8)
574
+
575
+ JSON, XML, form-encoded, plain text, and JavaScript are stored as UTF-8 strings
576
+ so you can read them directly:
577
+
578
+ ```bash
579
+ curl -X POST "http://localhost:3000/b/$ID" \
580
+ -H 'content-type: application/json' \
581
+ -d '{"hello":"world"}'
582
+
583
+ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodyEncoding, body}'
584
+ # {
585
+ # "bodyEncoding": "utf8",
586
+ # "body": "{\"hello\":\"world\"}"
587
+ # }
588
+ ```
589
+
590
+ ### Binary Content (Base64)
591
+
592
+ Any non-text content type is base64-encoded, preserving the bytes exactly:
593
+
594
+ ```bash
595
+ # Upload a PNG to the bin
596
+ curl -X POST "http://localhost:3000/b/$ID/upload" \
597
+ -H 'content-type: image/png' \
598
+ --data-binary @logo.png
599
+
600
+ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodyEncoding, bodySize}'
601
+ # {
602
+ # "bodyEncoding": "base64",
603
+ # "bodySize": 14823
604
+ # }
605
+
606
+ # Decode the body back to bytes
607
+ curl -s "http://localhost:3000/bins/$ID/requests" \
608
+ | jq -r '.requests[0].body' | base64 -d > recovered.png
609
+ ```
610
+
611
+ ### Truncation
612
+
613
+ Bodies larger than `maxBodySize` (1 MiB by default) are cut off and flagged:
614
+
615
+ ```javascript
616
+ // In test setup
617
+ import { MockHttp, BinManager } from '@jaredwray/mockhttp';
618
+ const mock = new MockHttp();
619
+ mock.bins = new BinManager({ maxBodySize: 1024 }); // 1 KiB cap
620
+ await mock.start();
621
+ ```
622
+
623
+ ```bash
624
+ # Send 5000 bytes to a bin with a 1 KiB cap
625
+ head -c 5000 /dev/urandom | curl -X POST "http://localhost:3000/b/$ID" \
626
+ -H 'content-type: application/octet-stream' \
627
+ --data-binary @-
628
+
629
+ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0] | {bodySize, truncated, body_len: (.body | length)}'
630
+ # {
631
+ # "bodySize": 5000, ← original size
632
+ # "truncated": true,
633
+ # "body_len": 1368 ← base64 of the first 1024 bytes
634
+ # }
635
+ ```
636
+
637
+ ## FIFO Eviction Example
638
+
639
+ Once a bin reaches `maxRequestsPerBin` captures (100 by default), the oldest
640
+ ones are dropped:
641
+
642
+ ```bash
643
+ # Send 105 requests
644
+ for i in $(seq 1 105); do
645
+ curl -s -X POST "http://localhost:3000/b/$ID/event/$i" > /dev/null
646
+ done
647
+
648
+ # The bin holds the most recent 100; the first 5 are gone
649
+ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests | length'
650
+ # 100
651
+
652
+ # Newest is at the top
653
+ curl -s "http://localhost:3000/bins/$ID/requests" | jq '.requests[0].path'
654
+ # "/event/105"
655
+ ```
656
+
657
+ ## Programmatic Access
658
+
659
+ The bin manager is exposed on the `MockHttp` instance as `mock.bins`. This
660
+ makes it easy to drive bins from a test suite without going through HTTP:
661
+
662
+ ```javascript
663
+ import { MockHttp } from '@jaredwray/mockhttp';
664
+
665
+ const mock = new MockHttp({ logging: false });
666
+ await mock.start();
667
+
668
+ // Create a bin
669
+ const bin = mock.bins.createBin();
670
+ console.log(`Webhook target: http://localhost:${mock.port}/b/${bin.id}`);
671
+
672
+ // ... point your code-under-test at that URL ...
673
+
674
+ // Read captured requests (newest first)
675
+ const requests = mock.bins.getRequests(bin.id);
676
+ console.log(`Captured ${requests.length} requests`);
677
+
678
+ for (const req of requests) {
679
+ console.log(`${req.method} ${req.path} (${req.bodySize} bytes)`);
680
+ }
681
+
682
+ // Clean up
683
+ mock.bins.deleteBin(bin.id);
684
+ await mock.close(); // also stops the bin cleanup timer
685
+ ```
686
+
687
+ ### Using Bins in a Vitest Test
688
+
689
+ ```typescript
690
+ import { afterAll, beforeAll, expect, test } from 'vitest';
691
+ import { MockHttp } from '@jaredwray/mockhttp';
692
+
693
+ let mock: MockHttp;
694
+
695
+ beforeAll(async () => {
696
+ mock = new MockHttp({ logging: false });
697
+ await mock.start();
698
+ });
699
+
700
+ afterAll(async () => {
701
+ await mock.close();
702
+ });
703
+
704
+ test('my SDK sends the right webhook payload', async () => {
705
+ const bin = mock.bins.createBin();
706
+ const webhookUrl = `http://localhost:${mock.port}/b/${bin.id}`;
707
+
708
+ // Drive your SDK at the bin
709
+ await mySdk.notify(webhookUrl, { event: 'user.created', id: 42 });
710
+
711
+ // Assert on what actually arrived
712
+ const [captured] = mock.bins.getRequests(bin.id);
713
+ expect(captured.method).toBe('POST');
714
+ expect(captured.contentType).toBe('application/json');
715
+ expect(JSON.parse(captured.body!)).toEqual({
716
+ event: 'user.created',
717
+ id: 42,
718
+ });
719
+ expect(captured.headers['x-signature']).toBeDefined();
720
+ });
721
+ ```
722
+
723
+ ## Configuration
724
+
725
+ Bins are enabled by default. To disable the routes entirely:
726
+
727
+ ```javascript
728
+ const mock = new MockHttp({
729
+ httpBin: { bins: false },
730
+ });
731
+ ```
732
+
733
+ To tune limits, replace the default `BinManager` before starting:
734
+
735
+ ```javascript
736
+ import { MockHttp, BinManager } from '@jaredwray/mockhttp';
737
+
738
+ const mock = new MockHttp();
739
+ mock.bins = new BinManager({
740
+ defaultTtlMs: 60 * 60 * 1000, // 1 hour (default: 24h)
741
+ maxRequestsPerBin: 500, // (default: 100)
742
+ maxBodySize: 5 * 1024 * 1024, // 5 MiB (default: 1 MiB)
743
+ idLength: 16, // (default: 12)
744
+ cleanupIntervalMs: 30 * 1000, // sweep every 30s (default: 60s)
745
+ });
746
+
747
+ await mock.start();
748
+ ```
749
+
750
+ ### Defaults
751
+
752
+ | Option | Default | Description |
753
+ |--------|---------|-------------|
754
+ | `defaultTtlMs` | `86400000` (24h) | Bin lifetime. Expired bins return 404 and are lazily removed. |
755
+ | `maxRequestsPerBin` | `100` | When exceeded, oldest captures are dropped (FIFO). |
756
+ | `maxBodySize` | `1048576` (1 MiB) | Larger bodies are truncated; `truncated: true` is set on the capture. |
757
+ | `idLength` | `12` | Length of generated bin and request ids. |
758
+ | `cleanupIntervalMs` | `60000` (1 min) | How often expired bins are swept. The timer is `unref()`'d so it never keeps the process alive. |
759
+
760
+ ### Pluggable Storage
761
+
762
+ `BinManager` accepts a `store: BinStore` so you can plug in alternative
763
+ backends (Redis, SQLite, etc.) without changing the rest of the codebase. The
764
+ default `InMemoryBinStore` keeps state in process memory.
765
+
766
+ ```typescript
767
+ import {
768
+ BinManager,
769
+ InMemoryBinStore,
770
+ type Bin,
771
+ type BinStore,
772
+ type CapturedRequest,
773
+ } from '@jaredwray/mockhttp';
774
+
775
+ class RedisBinStore implements BinStore {
776
+ createBin(bin: Bin): void { /* SET bin:${bin.id} ... */ }
777
+ getBin(id: string): Bin | undefined { /* GET ... */ }
778
+ listBins(): Bin[] { /* SCAN ... */ }
779
+ deleteBin(id: string): boolean { /* DEL ... */ }
780
+ addRequest(binId: string, req: CapturedRequest, max: number): void {
781
+ /* LPUSH bin:${binId}:requests + LTRIM to max */
782
+ }
783
+ getRequests(binId: string): CapturedRequest[] { /* LRANGE ... */ }
784
+ getRequest(binId: string, reqId: string): CapturedRequest | undefined { /* ... */ }
785
+ clearRequests(binId: string): void { /* DEL bin:${binId}:requests */ }
786
+ cleanupExpired(now: number): string[] { /* scan + delete */ return []; }
787
+ }
788
+
789
+ mock.bins = new BinManager({ store: new RedisBinStore() });
790
+ ```
791
+
434
792
  # Rate Limiting
435
793
 
436
794
  MockHttp supports rate limiting using [@fastify/rate-limit](https://github.com/fastify/fastify-rate-limit). Rate limiting is **enabled by default** at **1000 requests per minute** with **localhost (127.0.0.1 and ::1) excluded** from rate limiting.
@@ -654,6 +1012,7 @@ new MockHttp(options?)
654
1012
  - `anything?`: boolean - Enable anything routes (default: true)
655
1013
  - `auth?`: boolean - Enable authentication routes (default: true)
656
1014
  - `images?`: boolean - Enable image routes (default: true)
1015
+ - `bins?`: boolean - Enable request bin routes /bins and /b/:id (default: true)
657
1016
  - `https?`: boolean | HttpsOptions - Enable HTTPS with auto-generated or custom certificates (default: undefined/disabled)
658
1017
  - `http2?`: boolean - Enable HTTP/2 support (default: false)
659
1018
  - `http1?`: boolean - Allow HTTP/1.1 fallback when using HTTP/2 with HTTPS (default: true)
@@ -675,6 +1034,7 @@ new MockHttp(options?)
675
1034
  - `http1`: boolean - Get/set HTTP/1.1 fallback for HTTP/2 with HTTPS
676
1035
  - `server`: FastifyInstance - Get/set the Fastify server instance
677
1036
  - `taps`: TapManager - Get/set the TapManager instance
1037
+ - `bins`: BinManager - Get/set the BinManager instance for request bins
678
1038
 
679
1039
  ### Methods
680
1040
 
@@ -736,6 +1096,68 @@ Register authentication routes (basic, bearer, digest, hidden-basic).
736
1096
 
737
1097
  Register image routes (jpeg, png, svg, webp) with content negotiation support.
738
1098
 
1099
+ #### `async registerBinRoutes(fastifyInstance?)`
1100
+
1101
+ Register the request bin routes — management at `/bins` and capture at `/b/:id`.
1102
+
1103
+ ## Bins (Request Capture)
1104
+
1105
+ Access the BinManager via `mockHttp.bins` to manage request bins programmatically. See [Request Bins](#request-bins) for usage examples.
1106
+
1107
+ ### `bins.createBin()`
1108
+
1109
+ Create a new bin with the configured TTL.
1110
+
1111
+ **Returns:** `Bin` — `{ id, createdAt, expiresAt, requestCount }`
1112
+
1113
+ ### `bins.getBin(id)`
1114
+
1115
+ Look up a bin by id. Returns `undefined` if the bin does not exist or has expired (and lazily removes the expired entry).
1116
+
1117
+ **Returns:** `Bin | undefined`
1118
+
1119
+ ### `bins.listBins()`
1120
+
1121
+ List all non-expired bins.
1122
+
1123
+ **Returns:** `Bin[]`
1124
+
1125
+ ### `bins.deleteBin(id)`
1126
+
1127
+ Delete a bin and all its captured requests.
1128
+
1129
+ **Returns:** `boolean` — `true` if the bin existed
1130
+
1131
+ ### `bins.getRequests(binId)`
1132
+
1133
+ List captured requests for a bin, newest first.
1134
+
1135
+ **Returns:** `CapturedRequest[]`
1136
+
1137
+ ### `bins.getRequest(binId, reqId)`
1138
+
1139
+ Look up a single captured request.
1140
+
1141
+ **Returns:** `CapturedRequest | undefined`
1142
+
1143
+ ### `bins.clearRequests(binId)`
1144
+
1145
+ Clear all captured requests in a bin (the bin itself is preserved).
1146
+
1147
+ ### `bins.recordRequest(binId, raw)`
1148
+
1149
+ Manually record a captured request. Returns `undefined` if the bin does not exist or is expired. Normally invoked by the capture route, but available for programmatic use.
1150
+
1151
+ **Returns:** `CapturedRequest | undefined`
1152
+
1153
+ ### `bins.start()` / `bins.stop()`
1154
+
1155
+ Start or stop the periodic cleanup of expired bins. `MockHttp.start()` and `MockHttp.close()` call these automatically. The interval is `unref()`'d so it never keeps the Node process alive.
1156
+
1157
+ ### `bins.maxBodySize` / `bins.maxRequestsPerBin` / `bins.defaultTtlMs`
1158
+
1159
+ Read-only getters for the currently-configured limits.
1160
+
739
1161
  ## Taps (Response Injection)
740
1162
 
741
1163
  Access the TapManager via `mockHttp.taps` to inject custom responses.
package/dist/index.d.mts CHANGED
@@ -2,6 +2,147 @@ import { RateLimitPluginOptions } from "@fastify/rate-limit";
2
2
  import { FastifyInstance, FastifyRequest } from "fastify";
3
3
  import { Hookified, HookifiedOptions } from "hookified";
4
4
 
5
+ //#region src/bin-store.d.ts
6
+ /**
7
+ * A request captured by a bin, including method, headers, body and metadata.
8
+ */
9
+ type CapturedRequest = {
10
+ /** Unique identifier for this captured request */id: string; /** Identifier of the bin that captured this request */
11
+ binId: string; /** HTTP method (GET, POST, etc.) */
12
+ method: string; /** Request URL path + query, relative to the bin (e.g. "/foo?x=1") */
13
+ url: string; /** Sub-path after the bin id ("/" if the request hit the bin root) */
14
+ path: string; /** Parsed query string parameters */
15
+ query: Record<string, string | string[]>; /** Request headers */
16
+ headers: Record<string, string | string[]>; /** Client IP address as reported by Fastify */
17
+ remoteAddress: string; /** Content-Type header value, if any */
18
+ contentType?: string; /** Original body length in bytes */
19
+ bodySize: number; /** Body payload as utf8 text, base64-encoded bytes, or null when empty */
20
+ body: string | null; /** Encoding used for the `body` field */
21
+ bodyEncoding: "utf8" | "base64" | "none"; /** True if the body was longer than the configured `maxBodySize` */
22
+ truncated: boolean; /** ISO timestamp of when the request was captured */
23
+ capturedAt: string;
24
+ };
25
+ /**
26
+ * Metadata for a bin (without its captured requests).
27
+ */
28
+ type Bin = {
29
+ /** Unique short identifier for the bin */id: string; /** ISO timestamp of bin creation */
30
+ createdAt: string; /** ISO timestamp at which the bin expires */
31
+ expiresAt: string; /** Number of requests currently stored in the bin */
32
+ requestCount: number;
33
+ };
34
+ /**
35
+ * Storage backend for bins and their captured requests. The default
36
+ * implementation is in-memory; alternative backends (Redis, SQLite, etc.) can
37
+ * implement this interface without changes to the rest of the codebase.
38
+ */
39
+ interface BinStore {
40
+ /** Create a new bin record. */
41
+ createBin(bin: Bin): void;
42
+ /** Look up a bin by id. */
43
+ getBin(id: string): Bin | undefined;
44
+ /** Return all bins. */
45
+ listBins(): Bin[];
46
+ /** Remove a bin and all its captured requests. Returns true if removed. */
47
+ deleteBin(id: string): boolean;
48
+ /**
49
+ * Append a captured request to a bin, dropping the oldest if the bin has
50
+ * already reached `max` requests (FIFO).
51
+ */
52
+ addRequest(binId: string, req: CapturedRequest, max: number): void;
53
+ /** List captured requests for a bin (newest first). */
54
+ getRequests(binId: string): CapturedRequest[];
55
+ /** Look up a single captured request by id. */
56
+ getRequest(binId: string, reqId: string): CapturedRequest | undefined;
57
+ /** Clear all captured requests in a bin (the bin itself is preserved). */
58
+ clearRequests(binId: string): void;
59
+ /**
60
+ * Remove bins whose `expiresAt` is in the past relative to `now`.
61
+ * Returns the ids of bins that were removed.
62
+ */
63
+ cleanupExpired(now: number): string[];
64
+ }
65
+ /**
66
+ * In-memory implementation of {@link BinStore} backed by two Maps. State is
67
+ * lost when the process exits.
68
+ */
69
+ declare class InMemoryBinStore implements BinStore {
70
+ private readonly _bins;
71
+ private readonly _requests;
72
+ createBin(bin: Bin): void;
73
+ getBin(id: string): Bin | undefined;
74
+ listBins(): Bin[];
75
+ deleteBin(id: string): boolean;
76
+ addRequest(binId: string, req: CapturedRequest, max: number): void;
77
+ getRequests(binId: string): CapturedRequest[];
78
+ getRequest(binId: string, reqId: string): CapturedRequest | undefined;
79
+ clearRequests(binId: string): void;
80
+ cleanupExpired(now: number): string[];
81
+ }
82
+ //#endregion
83
+ //#region src/bin-manager.d.ts
84
+ /**
85
+ * Configuration options for {@link BinManager}.
86
+ */
87
+ type BinManagerOptions = {
88
+ /** Storage backend. Defaults to a new {@link InMemoryBinStore}. */store?: BinStore; /** Bin lifetime in milliseconds. Defaults to 24 hours. */
89
+ defaultTtlMs?: number; /** Maximum number of requests retained per bin (FIFO). Defaults to 100. */
90
+ maxRequestsPerBin?: number; /** Maximum captured body size in bytes. Larger bodies are truncated. Defaults to 1 MiB. */
91
+ maxBodySize?: number; /** Length of generated bin and request ids. Defaults to 12. */
92
+ idLength?: number; /** Interval between cleanup sweeps in milliseconds. Defaults to 1 minute. */
93
+ cleanupIntervalMs?: number;
94
+ };
95
+ /**
96
+ * Manages request bins: ephemeral URL endpoints that capture incoming HTTP
97
+ * requests so they can be inspected later.
98
+ */
99
+ declare class BinManager {
100
+ private readonly _store;
101
+ private readonly _defaultTtlMs;
102
+ private readonly _maxRequestsPerBin;
103
+ private readonly _maxBodySize;
104
+ private readonly _idLength;
105
+ private readonly _cleanupIntervalMs;
106
+ private _cleanupTimer;
107
+ constructor(options?: BinManagerOptions);
108
+ /** Underlying storage backend. */
109
+ get store(): BinStore;
110
+ /** Maximum captured body size in bytes. */
111
+ get maxBodySize(): number;
112
+ /** Maximum number of requests retained per bin. */
113
+ get maxRequestsPerBin(): number;
114
+ /** Default bin lifetime in milliseconds. */
115
+ get defaultTtlMs(): number;
116
+ /** Create a new bin with the configured TTL. */
117
+ createBin(): Bin;
118
+ /**
119
+ * Look up a bin by id. Returns `undefined` if the bin does not exist or
120
+ * has expired (in which case it is also lazily removed).
121
+ */
122
+ getBin(id: string): Bin | undefined;
123
+ /** List all non-expired bins. */
124
+ listBins(): Bin[];
125
+ /** Delete a bin. Returns true if the bin existed. */
126
+ deleteBin(id: string): boolean;
127
+ /**
128
+ * Record an incoming request against a bin. Returns the stored
129
+ * {@link CapturedRequest}, or `undefined` if the bin does not exist or is
130
+ * expired.
131
+ */
132
+ recordRequest(binId: string, raw: Omit<CapturedRequest, "id" | "binId" | "capturedAt">): CapturedRequest | undefined;
133
+ /** List captured requests for a bin (newest first). */
134
+ getRequests(binId: string): CapturedRequest[];
135
+ /** Look up a single captured request. */
136
+ getRequest(binId: string, reqId: string): CapturedRequest | undefined;
137
+ /** Clear all captured requests in a bin. */
138
+ clearRequests(binId: string): void;
139
+ /** Start the periodic cleanup of expired bins. */
140
+ start(): void;
141
+ /** Stop the periodic cleanup. Idempotent. */
142
+ stop(): void;
143
+ private generateId;
144
+ }
145
+ //#endregion
5
146
  //#region src/certificate.d.ts
6
147
  type CertificateOptions = {
7
148
  /**
@@ -191,6 +332,7 @@ type HttpBinOptions = {
191
332
  auth?: boolean;
192
333
  images?: boolean;
193
334
  dynamicData?: boolean;
335
+ bins?: boolean;
194
336
  };
195
337
  type HttpsOptions = {
196
338
  /**
@@ -282,6 +424,7 @@ declare class MockHttp extends Hookified {
282
424
  private _httpsCredentials;
283
425
  private _server;
284
426
  private _taps;
427
+ private _bins;
285
428
  constructor(options?: MockHttpOptions);
286
429
  /**
287
430
  * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
@@ -415,6 +558,14 @@ declare class MockHttp extends Hookified {
415
558
  * The TapManager instance for managing injection taps.
416
559
  */
417
560
  set taps(taps: TapManager);
561
+ /**
562
+ * The BinManager instance for managing request bins.
563
+ */
564
+ get bins(): BinManager;
565
+ /**
566
+ * The BinManager instance for managing request bins.
567
+ */
568
+ set bins(bins: BinManager);
418
569
  /**
419
570
  * Start the Fastify server. If the server is already running, it will be closed and restarted.
420
571
  */
@@ -484,6 +635,11 @@ declare class MockHttp extends Hookified {
484
635
  * @param fastifyInstance - the server instance to register the routes on.
485
636
  */
486
637
  registerDynamicDataRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
638
+ /**
639
+ * Register the request bin routes (management at /bins and capture at /b/:id).
640
+ * @param fastifyInstance - the server instance to register the routes on.
641
+ */
642
+ registerBinRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
487
643
  private resolveHttpsCredentials;
488
644
  private loadPemValue;
489
645
  }
@@ -491,4 +647,4 @@ declare class MockHttp extends Hookified {
491
647
  //#region src/index.d.ts
492
648
  declare const start: () => Promise<MockHttp>;
493
649
  //#endregion
494
- export { type CertificateFileOptions, type CertificateOptions, type CertificateResult, type HttpsOptions, type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
650
+ export { type Bin, BinManager, type BinManagerOptions, type BinStore, type CapturedRequest, type CertificateFileOptions, type CertificateOptions, type CertificateResult, type HttpsOptions, InMemoryBinStore, type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };