@jaredwray/mockhttp 1.3.0 → 1.4.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/README.md CHANGED
@@ -25,6 +25,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
25
25
  - [Deploy via Docker](#deploy-via-docker)
26
26
  - [Deploy via Docker Compose](#deploy-via-docker-compose)
27
27
  - [Deploy via NodeJS](#deploy-via-nodejs)
28
+ - [HTTPS Support](#https-support)
28
29
  - [Response Injection (Tap Feature)](#response-injection-tap-feature)
29
30
  - [Rate Limiting](#rate-limiting)
30
31
  - [Logging](#logging)
@@ -69,13 +70,133 @@ npm install @jaredwray/mockhttp --save
69
70
  then run `mockhttp` in your code.
70
71
 
71
72
  ```javascript
72
- import { mockhttp } from '@jaredwray/mockhttp';
73
- await mockhttp.start(); // start the server
73
+ import { MockHttp } from '@jaredwray/mockhttp';
74
+ const mock = new MockHttp();
75
+ await mock.start(); // start the server
74
76
  const response = await fetch('http://localhost:3000/get');
75
77
  console.log(response);
76
- await mockhttp.stop(); // stop the server
78
+ await mock.close(); // stop the server
79
+ ```
80
+
81
+ # HTTPS Support
82
+
83
+ MockHttp supports HTTPS with auto-generated self-signed certificates or your own custom certificates. No external dependencies are required — certificate generation uses only Node.js built-in `crypto`.
84
+
85
+ ## Auto-Generated Certificate
86
+
87
+ The simplest way to enable HTTPS is to pass `https: true`. A self-signed certificate for `localhost` is generated automatically:
88
+
89
+ ```javascript
90
+ import { MockHttp } from '@jaredwray/mockhttp';
91
+
92
+ const mock = new MockHttp({ https: true });
93
+ await mock.start();
94
+
95
+ console.log(mock.isHttps); // true
96
+
97
+ // Use Fastify's built-in inject() for testing (no TLS setup needed)
98
+ const response = await mock.server.inject({ method: 'GET', url: '/get' });
99
+ console.log(response.statusCode); // 200
100
+
101
+ await mock.close();
102
+ ```
103
+
104
+ > **Note:** Self-signed certificates are not trusted by default. When making real HTTPS requests (e.g. with `fetch`), set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your test environment or use a custom HTTPS agent.
105
+
106
+ ## Custom Certificate Options
107
+
108
+ You can customize the auto-generated certificate by passing `certificateOptions`:
109
+
110
+ ```javascript
111
+ const mock = new MockHttp({
112
+ https: {
113
+ certificateOptions: {
114
+ commonName: 'my-test-server',
115
+ validityDays: 30,
116
+ keySize: 4096,
117
+ altNames: [
118
+ { type: 'dns', value: 'example.local' },
119
+ { type: 'dns', value: '*.example.local' },
120
+ { type: 'ip', value: '192.168.1.100' },
121
+ ],
122
+ },
123
+ },
124
+ });
125
+
126
+ await mock.start();
127
+ // Make requests...
128
+ await mock.close();
129
+ ```
130
+
131
+ ## Provide Your Own Certificate
132
+
133
+ You can supply your own PEM-encoded certificate and key, either as strings or file paths:
134
+
135
+ ```javascript
136
+ // Using PEM strings
137
+ const mock = new MockHttp({
138
+ https: {
139
+ cert: '-----BEGIN CERTIFICATE-----\n...',
140
+ key: '-----BEGIN PRIVATE KEY-----\n...',
141
+ },
142
+ });
143
+ await mock.start();
144
+ // Make requests...
145
+ await mock.close();
146
+ ```
147
+
148
+ ```javascript
149
+ // Using file paths
150
+ const mock = new MockHttp({
151
+ https: {
152
+ cert: '/path/to/cert.pem',
153
+ key: '/path/to/key.pem',
154
+ },
155
+ });
156
+ await mock.start();
157
+ // Make requests...
158
+ await mock.close();
159
+ ```
160
+
161
+ ## Standalone Certificate Generation
162
+
163
+ You can also generate certificates independently using the exported utility functions:
164
+
165
+ ```javascript
166
+ import { generateCertificate, generateCertificateFiles } from '@jaredwray/mockhttp';
167
+
168
+ // Generate in-memory PEM strings
169
+ const { cert, key } = generateCertificate({
170
+ commonName: 'my-app',
171
+ validityDays: 90,
172
+ });
173
+
174
+ // Generate and write to disk
175
+ const result = await generateCertificateFiles({
176
+ certPath: './certs/cert.pem',
177
+ keyPath: './certs/key.pem',
178
+ commonName: 'my-app',
179
+ });
77
180
  ```
78
181
 
182
+ ## HTTPS Options Reference
183
+
184
+ | Option | Type | Default | Description |
185
+ |--------|------|---------|-------------|
186
+ | `cert` | string | — | PEM-encoded certificate string or file path |
187
+ | `key` | string | — | PEM-encoded private key string or file path |
188
+ | `autoGenerate` | boolean | `true` | Auto-generate a self-signed certificate when cert/key are not provided |
189
+ | `certificateOptions` | CertificateOptions | — | Options for the auto-generated certificate |
190
+
191
+ ### Certificate Options
192
+
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `commonName` | string | `'localhost'` | Certificate subject Common Name (CN) |
196
+ | `altNames` | Array\<{ type, value }\> | `[dns:localhost, ip:127.0.0.1, ip:::1]` | Subject Alternative Names with type `'dns'` or `'ip'` |
197
+ | `validityDays` | number | `365` | Certificate validity period in days |
198
+ | `keySize` | number | `2048` | RSA key size in bits |
199
+
79
200
  # Response Injection (Tap Feature)
80
201
 
81
202
  The injection/tap feature allows you to "tap into" the request flow and inject custom responses for specific requests. This is particularly useful for:
@@ -467,6 +588,7 @@ new MockHttp(options?)
467
588
  - `anything?`: boolean - Enable anything routes (default: true)
468
589
  - `auth?`: boolean - Enable authentication routes (default: true)
469
590
  - `images?`: boolean - Enable image routes (default: true)
591
+ - `https?`: boolean | HttpsOptions - Enable HTTPS with auto-generated or custom certificates (default: undefined/disabled)
470
592
  - `hookOptions?`: HookifiedOptions - Hookified options
471
593
 
472
594
  ### Properties
@@ -479,6 +601,8 @@ new MockHttp(options?)
479
601
  - `logging`: boolean - Get/set logging enabled state
480
602
  - `rateLimit`: RateLimitPluginOptions | undefined - Get/set rate limiting options
481
603
  - `httpBin`: HttpBinOptions - Get/set httpbin route options
604
+ - `https`: HttpsOptions | undefined - Get/set HTTPS configuration
605
+ - `isHttps`: boolean - Whether the server is running with HTTPS
482
606
  - `server`: FastifyInstance - Get/set the Fastify server instance
483
607
  - `taps`: TapManager - Get/set the TapManager instance
484
608
 
package/dist/index.d.ts CHANGED
@@ -2,6 +2,61 @@ import { RateLimitPluginOptions } from '@fastify/rate-limit';
2
2
  import { FastifyRequest, FastifyInstance } from 'fastify';
3
3
  import { Hookified, HookifiedOptions } from 'hookified';
4
4
 
5
+ type CertificateOptions = {
6
+ /**
7
+ * Common Name for the certificate subject. Defaults to 'localhost'.
8
+ */
9
+ commonName?: string;
10
+ /**
11
+ * Subject Alternative Names. Defaults to [dns:localhost, ip:127.0.0.1, ip:::1].
12
+ */
13
+ altNames?: Array<{
14
+ type: "dns";
15
+ value: string;
16
+ } | {
17
+ type: "ip";
18
+ value: string;
19
+ }>;
20
+ /**
21
+ * Number of days the certificate is valid. Defaults to 365.
22
+ */
23
+ validityDays?: number;
24
+ /**
25
+ * RSA key size in bits. Defaults to 2048.
26
+ */
27
+ keySize?: number;
28
+ };
29
+ type CertificateResult = {
30
+ /**
31
+ * PEM-encoded X.509 certificate.
32
+ */
33
+ cert: string;
34
+ /**
35
+ * PEM-encoded PKCS#8 private key.
36
+ */
37
+ key: string;
38
+ };
39
+ type CertificateFileOptions = CertificateOptions & {
40
+ /**
41
+ * File path to write the certificate PEM.
42
+ */
43
+ certPath: string;
44
+ /**
45
+ * File path to write the private key PEM.
46
+ */
47
+ keyPath: string;
48
+ };
49
+ /**
50
+ * Generate a self-signed certificate using only Node.js built-in crypto.
51
+ * Returns PEM-encoded certificate and private key strings.
52
+ */
53
+ declare function generateCertificate(options?: CertificateOptions): CertificateResult;
54
+ /**
55
+ * Generate a self-signed certificate and write PEM files to disk.
56
+ * Returns the same CertificateResult as generateCertificate().
57
+ */
58
+ declare function generateCertificateFiles(options: CertificateFileOptions): Promise<CertificateResult>;
59
+
5
60
  /**
6
61
  * Configuration for the injected response
7
62
  */
@@ -134,6 +189,24 @@ type HttpBinOptions = {
134
189
  images?: boolean;
135
190
  dynamicData?: boolean;
136
191
  };
192
+ type HttpsOptions = {
193
+ /**
194
+ * PEM-encoded certificate string, or file path to a certificate PEM file.
195
+ */
196
+ cert?: string;
197
+ /**
198
+ * PEM-encoded private key string, or file path to a private key PEM file.
199
+ */
200
+ key?: string;
201
+ /**
202
+ * If true, auto-generate a self-signed certificate. Defaults to true when no cert/key provided.
203
+ */
204
+ autoGenerate?: boolean;
205
+ /**
206
+ * Options for auto-generating a self-signed certificate. Only used when auto-generating.
207
+ */
208
+ certificateOptions?: CertificateOptions;
209
+ };
137
210
  type MockHttpOptions = {
138
211
  /**
139
212
  * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
@@ -173,6 +246,11 @@ type MockHttpOptions = {
173
246
  * Whether to enable logging. Defaults to true.
174
247
  */
175
248
  logging?: boolean;
249
+ /**
250
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
251
+ * or provide an object with cert/key strings/paths or auto-generation options.
252
+ */
253
+ https?: boolean | HttpsOptions;
176
254
  };
177
255
  declare class MockHttp extends Hookified {
178
256
  private _port;
@@ -183,6 +261,8 @@ declare class MockHttp extends Hookified {
183
261
  private _logging;
184
262
  private _httpBin;
185
263
  private _rateLimit?;
264
+ private _https;
265
+ private _httpsCredentials;
186
266
  private _server;
187
267
  private _taps;
188
268
  constructor(options?: MockHttpOptions);
@@ -246,6 +326,20 @@ declare class MockHttp extends Hookified {
246
326
  * @default true
247
327
  */
248
328
  set logging(logging: boolean);
329
+ /**
330
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
331
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
332
+ */
333
+ get https(): HttpsOptions | undefined;
334
+ /**
335
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
336
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
337
+ */
338
+ set https(value: boolean | HttpsOptions | undefined);
339
+ /**
340
+ * Whether the server is running in HTTPS mode.
341
+ */
342
+ get isHttps(): boolean;
249
343
  /**
250
344
  * HTTP Bin options. Defaults to all enabled.
251
345
  */
@@ -353,8 +447,10 @@ declare class MockHttp extends Hookified {
353
447
  * @param fastifyInstance - the server instance to register the routes on.
354
448
  */
355
449
  registerDynamicDataRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
450
+ private resolveHttpsCredentials;
451
+ private loadPemValue;
356
452
  }
357
453
 
358
454
  declare const start: () => Promise<MockHttp>;
359
455
 
360
- export { type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, MockHttp as mockhttp, start };
456
+ export { type CertificateFileOptions, type CertificateOptions, type CertificateResult, type HttpsOptions, type InjectionMatcher, type InjectionResponse, type InjectionTap, MockHttp as default, generateCertificate, generateCertificateFiles, MockHttp as mockhttp, start };
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
5
5
  import process2 from "process";
6
6
 
7
7
  // src/mock-http.ts
8
+ import * as fsPromises2 from "fs/promises";
8
9
  import path from "path";
9
10
  import fastifyCookie from "@fastify/cookie";
10
11
  import fastifyHelmet from "@fastify/helmet";
@@ -15,6 +16,307 @@ import { detect } from "detect-port";
15
16
  import Fastify from "fastify";
16
17
  import { Hookified } from "hookified";
17
18
 
19
+ // src/certificate.ts
20
+ import * as crypto from "crypto";
21
+ import * as fsPromises from "fs/promises";
22
+ function encodeLength(length) {
23
+ if (length < 128) {
24
+ return Buffer.from([
25
+ length
26
+ ]);
27
+ }
28
+ const bytes = [];
29
+ let temp = length;
30
+ while (temp > 0) {
31
+ bytes.unshift(temp & 255);
32
+ temp >>= 8;
33
+ }
34
+ return Buffer.from([
35
+ 128 | bytes.length,
36
+ ...bytes
37
+ ]);
38
+ }
39
+ __name(encodeLength, "encodeLength");
40
+ function encodeTlv(tag, value) {
41
+ return Buffer.concat([
42
+ Buffer.from([
43
+ tag
44
+ ]),
45
+ encodeLength(value.length),
46
+ value
47
+ ]);
48
+ }
49
+ __name(encodeTlv, "encodeTlv");
50
+ function encodeSequence(...elements) {
51
+ const content = Buffer.concat(elements);
52
+ return encodeTlv(48, content);
53
+ }
54
+ __name(encodeSequence, "encodeSequence");
55
+ function encodeSet(...elements) {
56
+ const content = Buffer.concat(elements);
57
+ return encodeTlv(49, content);
58
+ }
59
+ __name(encodeSet, "encodeSet");
60
+ function encodeInteger(value) {
61
+ let buf;
62
+ if (typeof value === "number") {
63
+ if (value === 0) {
64
+ buf = Buffer.from([
65
+ 0
66
+ ]);
67
+ } else {
68
+ const bytes = [];
69
+ let temp = value;
70
+ while (temp > 0) {
71
+ bytes.unshift(temp & 255);
72
+ temp >>= 8;
73
+ }
74
+ if (bytes[0] & 128) {
75
+ bytes.unshift(0);
76
+ }
77
+ buf = Buffer.from(bytes);
78
+ }
79
+ } else {
80
+ if (value.length > 0 && value[0] & 128) {
81
+ buf = Buffer.concat([
82
+ Buffer.from([
83
+ 0
84
+ ]),
85
+ value
86
+ ]);
87
+ } else {
88
+ buf = value;
89
+ }
90
+ }
91
+ return encodeTlv(2, buf);
92
+ }
93
+ __name(encodeInteger, "encodeInteger");
94
+ function encodeBitString(value) {
95
+ const content = Buffer.concat([
96
+ Buffer.from([
97
+ 0
98
+ ]),
99
+ value
100
+ ]);
101
+ return encodeTlv(3, content);
102
+ }
103
+ __name(encodeBitString, "encodeBitString");
104
+ function encodeOctetString(value) {
105
+ return encodeTlv(4, value);
106
+ }
107
+ __name(encodeOctetString, "encodeOctetString");
108
+ function encodeOid(oid) {
109
+ const bytes = [];
110
+ bytes.push(40 * oid[0] + oid[1]);
111
+ for (let i = 2; i < oid.length; i++) {
112
+ let component = oid[i];
113
+ if (component < 128) {
114
+ bytes.push(component);
115
+ } else {
116
+ const encoded = [];
117
+ encoded.push(component & 127);
118
+ component >>= 7;
119
+ while (component > 0) {
120
+ encoded.push(component & 127 | 128);
121
+ component >>= 7;
122
+ }
123
+ encoded.reverse();
124
+ bytes.push(...encoded);
125
+ }
126
+ }
127
+ return encodeTlv(6, Buffer.from(bytes));
128
+ }
129
+ __name(encodeOid, "encodeOid");
130
+ function encodeUtf8String(value) {
131
+ return encodeTlv(12, Buffer.from(value, "utf8"));
132
+ }
133
+ __name(encodeUtf8String, "encodeUtf8String");
134
+ function encodeTime(date) {
135
+ const fullYear = date.getUTCFullYear();
136
+ const timePart = `${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
137
+ if (fullYear >= 2050) {
138
+ const str2 = `${fullYear}${timePart}`;
139
+ return encodeTlv(24, Buffer.from(str2, "ascii"));
140
+ }
141
+ const str = `${pad2(fullYear % 100)}${timePart}`;
142
+ return encodeTlv(23, Buffer.from(str, "ascii"));
143
+ }
144
+ __name(encodeTime, "encodeTime");
145
+ function encodeContextSpecific(tag, value) {
146
+ return encodeTlv(160 | tag, value);
147
+ }
148
+ __name(encodeContextSpecific, "encodeContextSpecific");
149
+ function pad2(n) {
150
+ return n.toString().padStart(2, "0");
151
+ }
152
+ __name(pad2, "pad2");
153
+ var OID_SHA256_WITH_RSA = [
154
+ 1,
155
+ 2,
156
+ 840,
157
+ 113549,
158
+ 1,
159
+ 1,
160
+ 11
161
+ ];
162
+ var OID_COMMON_NAME = [
163
+ 2,
164
+ 5,
165
+ 4,
166
+ 3
167
+ ];
168
+ var OID_SUBJECT_ALT_NAME = [
169
+ 2,
170
+ 5,
171
+ 29,
172
+ 17
173
+ ];
174
+ function buildAlgorithmIdentifier() {
175
+ return encodeSequence(encodeOid(OID_SHA256_WITH_RSA), encodeTlv(5, Buffer.alloc(0)));
176
+ }
177
+ __name(buildAlgorithmIdentifier, "buildAlgorithmIdentifier");
178
+ function buildName(cn) {
179
+ const rdn = encodeSet(encodeSequence(encodeOid(OID_COMMON_NAME), encodeUtf8String(cn)));
180
+ return encodeSequence(rdn);
181
+ }
182
+ __name(buildName, "buildName");
183
+ function buildValidity(notBefore, notAfter) {
184
+ return encodeSequence(encodeTime(notBefore), encodeTime(notAfter));
185
+ }
186
+ __name(buildValidity, "buildValidity");
187
+ function encodeIpAddress(ip) {
188
+ if (ip.includes(".")) {
189
+ const parts = ip.split(".").map(Number);
190
+ return Buffer.from(parts);
191
+ }
192
+ const expanded = expandIpv6(ip);
193
+ const buf = Buffer.alloc(16);
194
+ const groups = expanded.split(":");
195
+ for (let i = 0; i < 8; i++) {
196
+ const val = Number.parseInt(groups[i], 16);
197
+ buf.writeUInt16BE(val, i * 2);
198
+ }
199
+ return buf;
200
+ }
201
+ __name(encodeIpAddress, "encodeIpAddress");
202
+ function expandIpv6(ip) {
203
+ if (ip.includes("::")) {
204
+ const [left, right] = ip.split("::");
205
+ const leftGroups = left ? left.split(":") : [];
206
+ const rightGroups = right ? right.split(":") : [];
207
+ const missing = 8 - leftGroups.length - rightGroups.length;
208
+ const middle = Array.from({
209
+ length: missing
210
+ }).fill("0000");
211
+ const allGroups = [
212
+ ...leftGroups,
213
+ ...middle,
214
+ ...rightGroups
215
+ ];
216
+ return allGroups.map((g) => g.padStart(4, "0")).join(":");
217
+ }
218
+ return ip.split(":").map((g) => g.padStart(4, "0")).join(":");
219
+ }
220
+ __name(expandIpv6, "expandIpv6");
221
+ function buildSubjectAltNameExtension(altNames) {
222
+ const names = [];
223
+ for (const alt of altNames) {
224
+ if (alt.type === "dns") {
225
+ names.push(encodeTlv(130, Buffer.from(alt.value, "ascii")));
226
+ } else {
227
+ names.push(encodeTlv(135, encodeIpAddress(alt.value)));
228
+ }
229
+ }
230
+ const sanValue = encodeSequence(...names);
231
+ return encodeSequence(encodeOid(OID_SUBJECT_ALT_NAME), encodeOctetString(sanValue));
232
+ }
233
+ __name(buildSubjectAltNameExtension, "buildSubjectAltNameExtension");
234
+ function buildExtensions(altNames) {
235
+ const extensions = encodeSequence(buildSubjectAltNameExtension(altNames));
236
+ return encodeContextSpecific(3, extensions);
237
+ }
238
+ __name(buildExtensions, "buildExtensions");
239
+ function buildTbsCertificate(serialNumber, issuerCn, notBefore, notAfter, subjectCn, publicKeyDer, altNames) {
240
+ const version = encodeContextSpecific(0, encodeInteger(2));
241
+ const serial = encodeInteger(serialNumber);
242
+ const signatureAlgorithm = buildAlgorithmIdentifier();
243
+ const issuer = buildName(issuerCn);
244
+ const validity = buildValidity(notBefore, notAfter);
245
+ const subject = buildName(subjectCn);
246
+ const extensions = buildExtensions(altNames);
247
+ return encodeSequence(version, serial, signatureAlgorithm, issuer, validity, subject, publicKeyDer, extensions);
248
+ }
249
+ __name(buildTbsCertificate, "buildTbsCertificate");
250
+ function derToPem(der, label) {
251
+ const base64 = der.toString("base64");
252
+ const lines = [];
253
+ for (let i = 0; i < base64.length; i += 64) {
254
+ lines.push(base64.slice(i, i + 64));
255
+ }
256
+ return `-----BEGIN ${label}-----
257
+ ${lines.join("\n")}
258
+ -----END ${label}-----
259
+ `;
260
+ }
261
+ __name(derToPem, "derToPem");
262
+ function generateCertificate(options) {
263
+ const commonName = options?.commonName ?? "localhost";
264
+ const validityDays = options?.validityDays ?? 365;
265
+ const keySize = options?.keySize ?? 2048;
266
+ const altNames = options?.altNames ?? [
267
+ {
268
+ type: "dns",
269
+ value: "localhost"
270
+ },
271
+ {
272
+ type: "ip",
273
+ value: "127.0.0.1"
274
+ },
275
+ {
276
+ type: "ip",
277
+ value: "::1"
278
+ }
279
+ ];
280
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
281
+ modulusLength: keySize,
282
+ publicKeyEncoding: {
283
+ type: "spki",
284
+ format: "der"
285
+ },
286
+ privateKeyEncoding: {
287
+ type: "pkcs8",
288
+ format: "pem"
289
+ }
290
+ });
291
+ const notBefore = /* @__PURE__ */ new Date();
292
+ const notAfter = /* @__PURE__ */ new Date();
293
+ notAfter.setDate(notAfter.getDate() + validityDays);
294
+ const serialBytes = crypto.randomBytes(16);
295
+ serialBytes[0] &= 127;
296
+ let start2 = 0;
297
+ while (start2 < serialBytes.length - 1 && serialBytes[start2] === 0) {
298
+ start2++;
299
+ }
300
+ const serialNumber = serialBytes.subarray(start2);
301
+ const tbsCertificate = buildTbsCertificate(serialNumber, commonName, notBefore, notAfter, commonName, publicKey, altNames);
302
+ const signer = crypto.createSign("SHA256");
303
+ signer.update(tbsCertificate);
304
+ const signature = signer.sign(privateKey);
305
+ const certificate = encodeSequence(tbsCertificate, buildAlgorithmIdentifier(), encodeBitString(signature));
306
+ return {
307
+ cert: derToPem(certificate, "CERTIFICATE"),
308
+ key: privateKey
309
+ };
310
+ }
311
+ __name(generateCertificate, "generateCertificate");
312
+ async function generateCertificateFiles(options) {
313
+ const result = generateCertificate(options);
314
+ await fsPromises.writeFile(options.certPath, result.cert, "utf8");
315
+ await fsPromises.writeFile(options.keyPath, result.key, "utf8");
316
+ return result;
317
+ }
318
+ __name(generateCertificateFiles, "generateCertificateFiles");
319
+
18
320
  // src/fastify-config.ts
19
321
  function getFastifyConfig(logging = true) {
20
322
  return {
@@ -328,8 +630,8 @@ var bearerAuthRoute = /* @__PURE__ */ __name((fastify) => {
328
630
  }, "bearerAuthRoute");
329
631
 
330
632
  // src/routes/auth/digest.ts
331
- import crypto from "crypto";
332
- var makeNonce = /* @__PURE__ */ __name(() => crypto.randomBytes(16).toString("hex"), "makeNonce");
633
+ import crypto2 from "crypto";
634
+ var makeNonce = /* @__PURE__ */ __name(() => crypto2.randomBytes(16).toString("hex"), "makeNonce");
333
635
  var algorithms = /* @__PURE__ */ new Set([
334
636
  "MD5",
335
637
  "md5",
@@ -354,7 +656,7 @@ var hashAlg = /* @__PURE__ */ __name((alg) => {
354
656
  }
355
657
  return "md5";
356
658
  }, "hashAlg");
357
- var h = /* @__PURE__ */ __name((algo, data) => crypto.createHash(algo).update(data).digest("hex"), "h");
659
+ var h = /* @__PURE__ */ __name((algo, data) => crypto2.createHash(algo).update(data).digest("hex"), "h");
358
660
  var parseDigest = /* @__PURE__ */ __name((header) => {
359
661
  if (!header) {
360
662
  return void 0;
@@ -771,7 +1073,7 @@ var base64Route = /* @__PURE__ */ __name((fastify) => {
771
1073
  }, "base64Route");
772
1074
 
773
1075
  // src/routes/dynamic-data/bytes.ts
774
- import { randomBytes } from "crypto";
1076
+ import { randomBytes as randomBytes2 } from "crypto";
775
1077
  var bytesSchema = {
776
1078
  description: "Returns n random bytes generated with given seed",
777
1079
  tags: [
@@ -853,7 +1155,7 @@ var bytesRoute = /* @__PURE__ */ __name((fastify) => {
853
1155
  }
854
1156
  bytes = generateSeededBytes(limitedN, seed);
855
1157
  } else {
856
- bytes = randomBytes(limitedN);
1158
+ bytes = randomBytes2(limitedN);
857
1159
  }
858
1160
  return reply.header("Content-Type", "application/octet-stream").header("Content-Length", bytes.length).send(bytes);
859
1161
  });
@@ -1375,7 +1677,7 @@ var streamRoute = /* @__PURE__ */ __name((fastify) => {
1375
1677
  }, "streamRoute");
1376
1678
 
1377
1679
  // src/routes/dynamic-data/stream-bytes.ts
1378
- import { randomBytes as randomBytes2 } from "crypto";
1680
+ import { randomBytes as randomBytes3 } from "crypto";
1379
1681
  var streamBytesSchema = {
1380
1682
  description: "Streams n random bytes generated with given seed, in chunks of chunk_size per packet",
1381
1683
  tags: [
@@ -1468,7 +1770,7 @@ var streamBytesRoute = /* @__PURE__ */ __name((fastify) => {
1468
1770
  }
1469
1771
  allBytes = generateSeededBytes2(limitedN, seed);
1470
1772
  } else {
1471
- allBytes = randomBytes2(limitedN);
1773
+ allBytes = randomBytes3(limitedN);
1472
1774
  }
1473
1775
  reply.raw.writeHead(200, {
1474
1776
  "Content-Type": "application/octet-stream",
@@ -3530,7 +3832,7 @@ import { fastifySwaggerUi } from "@fastify/swagger-ui";
3530
3832
  // package.json
3531
3833
  var package_default = {
3532
3834
  name: "@jaredwray/mockhttp",
3533
- version: "1.3.0",
3835
+ version: "1.4.0",
3534
3836
  description: "Mock Http - Easy way to mock http with httpbin replacement",
3535
3837
  type: "module",
3536
3838
  main: "dist/index.js",
@@ -3581,29 +3883,29 @@ var package_default = {
3581
3883
  },
3582
3884
  homepage: "https://github.com/jaredwray/mockhttp#readme",
3583
3885
  devDependencies: {
3584
- "@biomejs/biome": "^2.3.11",
3585
- "@swc/core": "^1.15.8",
3886
+ "@biomejs/biome": "^2.3.14",
3887
+ "@swc/core": "^1.15.11",
3586
3888
  "@types/html-escaper": "^3.0.4",
3587
- "@vitest/coverage-v8": "^4.0.16",
3889
+ "@vitest/coverage-v8": "^4.0.18",
3588
3890
  rimraf: "^6.1.2",
3589
3891
  tsup: "^8.5.1",
3590
3892
  tsx: "^4.21.0",
3591
3893
  typescript: "^5.9.3",
3592
- vitest: "^4.0.16"
3894
+ vitest: "^4.0.18"
3593
3895
  },
3594
3896
  dependencies: {
3595
3897
  "@fastify/cookie": "^11.0.2",
3596
3898
  "@fastify/helmet": "^13.0.2",
3597
3899
  "@fastify/rate-limit": "^10.3.0",
3598
3900
  "@fastify/static": "^9.0.0",
3599
- "@fastify/swagger": "^9.6.1",
3600
- "@fastify/swagger-ui": "^5.2.4",
3601
- "@scalar/api-reference": "^1.43.1",
3901
+ "@fastify/swagger": "^9.7.0",
3902
+ "@fastify/swagger-ui": "^5.2.5",
3903
+ "@scalar/api-reference": "^1.44.16",
3602
3904
  "detect-port": "^2.1.0",
3603
- fastify: "^5.6.2",
3604
- hookified: "^1.15.0",
3905
+ fastify: "^5.7.4",
3906
+ hookified: "^1.15.1",
3605
3907
  "html-escaper": "^3.0.3",
3606
- pino: "^10.1.1",
3908
+ pino: "^10.3.1",
3607
3909
  "pino-pretty": "^13.1.3"
3608
3910
  },
3609
3911
  files: [
@@ -3822,6 +4124,8 @@ var MockHttp = class extends Hookified {
3822
4124
  "::1"
3823
4125
  ]
3824
4126
  };
4127
+ _https;
4128
+ _httpsCredentials;
3825
4129
  _server = Fastify();
3826
4130
  _taps = new TapManager();
3827
4131
  constructor(options) {
@@ -3851,6 +4155,17 @@ var MockHttp = class extends Hookified {
3851
4155
  if (options?.logging !== void 0) {
3852
4156
  this._logging = options.logging;
3853
4157
  }
4158
+ if (options?.https !== void 0) {
4159
+ if (options.https === true) {
4160
+ this._https = {
4161
+ autoGenerate: true
4162
+ };
4163
+ } else if (options.https === false) {
4164
+ this._https = void 0;
4165
+ } else {
4166
+ this._https = options.https;
4167
+ }
4168
+ }
3854
4169
  }
3855
4170
  /**
3856
4171
  * The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
@@ -3937,6 +4252,34 @@ var MockHttp = class extends Hookified {
3937
4252
  this._logging = logging;
3938
4253
  }
3939
4254
  /**
4255
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
4256
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
4257
+ */
4258
+ get https() {
4259
+ return this._https;
4260
+ }
4261
+ /**
4262
+ * HTTPS configuration. Set to true to auto-generate a self-signed certificate,
4263
+ * or provide an HttpsOptions object with cert/key or auto-generation options.
4264
+ */
4265
+ set https(value) {
4266
+ if (value === true) {
4267
+ this._https = {
4268
+ autoGenerate: true
4269
+ };
4270
+ } else if (value === false || value === void 0) {
4271
+ this._https = void 0;
4272
+ } else {
4273
+ this._https = value;
4274
+ }
4275
+ }
4276
+ /**
4277
+ * Whether the server is running in HTTPS mode.
4278
+ */
4279
+ get isHttps() {
4280
+ return this._httpsCredentials !== void 0;
4281
+ }
4282
+ /**
3940
4283
  * HTTP Bin options. Defaults to all enabled.
3941
4284
  */
3942
4285
  get httpBin() {
@@ -3994,11 +4337,25 @@ var MockHttp = class extends Hookified {
3994
4337
  * Start the Fastify server. If the server is already running, it will be closed and restarted.
3995
4338
  */
3996
4339
  async start() {
4340
+ this._httpsCredentials = void 0;
4341
+ if (this._https) {
4342
+ this._httpsCredentials = await this.resolveHttpsCredentials(this._https);
4343
+ }
3997
4344
  try {
3998
4345
  if (this._server) {
3999
4346
  await this._server.close();
4000
4347
  }
4001
- this._server = Fastify(getFastifyConfig(this._logging));
4348
+ if (this._httpsCredentials) {
4349
+ this._server = Fastify({
4350
+ ...getFastifyConfig(this._logging),
4351
+ https: {
4352
+ key: this._httpsCredentials.key,
4353
+ cert: this._httpsCredentials.cert
4354
+ }
4355
+ });
4356
+ } else {
4357
+ this._server = Fastify(getFastifyConfig(this._logging));
4358
+ }
4002
4359
  this._server.addHook("onRequest", async (request, reply) => {
4003
4360
  const matchedTap = this._taps.matchRequest(request);
4004
4361
  if (matchedTap) {
@@ -4248,6 +4605,33 @@ var MockHttp = class extends Hookified {
4248
4605
  await fastify.register(dripRoute);
4249
4606
  await fastify.register(linksRoute);
4250
4607
  }
4608
+ async resolveHttpsCredentials(options) {
4609
+ if (options.cert && options.key) {
4610
+ const cert = await this.loadPemValue(options.cert);
4611
+ const key = await this.loadPemValue(options.key);
4612
+ return {
4613
+ cert,
4614
+ key
4615
+ };
4616
+ }
4617
+ if ((options.cert || options.key) && options.autoGenerate !== true) {
4618
+ throw new Error("HTTPS options must include both 'cert' and 'key'. Only one was provided.");
4619
+ }
4620
+ if (options.autoGenerate !== false) {
4621
+ const result = generateCertificate(options.certificateOptions);
4622
+ return {
4623
+ cert: result.cert,
4624
+ key: result.key
4625
+ };
4626
+ }
4627
+ throw new Error("HTTPS is enabled but no certificate was provided and autoGenerate is false.");
4628
+ }
4629
+ async loadPemValue(value) {
4630
+ if (value.startsWith("-----")) {
4631
+ return value;
4632
+ }
4633
+ return fsPromises2.readFile(value, "utf8");
4634
+ }
4251
4635
  };
4252
4636
 
4253
4637
  // src/index.ts
@@ -4270,6 +4654,8 @@ if (import.meta.url === `file://${process2.argv[1]}`) {
4270
4654
  }
4271
4655
  export {
4272
4656
  MockHttp as default,
4657
+ generateCertificate,
4658
+ generateCertificateFiles,
4273
4659
  MockHttp as mockhttp,
4274
4660
  start
4275
4661
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaredwray/mockhttp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Mock Http - Easy way to mock http with httpbin replacement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,29 +51,29 @@
51
51
  },
52
52
  "homepage": "https://github.com/jaredwray/mockhttp#readme",
53
53
  "devDependencies": {
54
- "@biomejs/biome": "^2.3.11",
55
- "@swc/core": "^1.15.8",
54
+ "@biomejs/biome": "^2.3.14",
55
+ "@swc/core": "^1.15.11",
56
56
  "@types/html-escaper": "^3.0.4",
57
- "@vitest/coverage-v8": "^4.0.16",
57
+ "@vitest/coverage-v8": "^4.0.18",
58
58
  "rimraf": "^6.1.2",
59
59
  "tsup": "^8.5.1",
60
60
  "tsx": "^4.21.0",
61
61
  "typescript": "^5.9.3",
62
- "vitest": "^4.0.16"
62
+ "vitest": "^4.0.18"
63
63
  },
64
64
  "dependencies": {
65
65
  "@fastify/cookie": "^11.0.2",
66
66
  "@fastify/helmet": "^13.0.2",
67
67
  "@fastify/rate-limit": "^10.3.0",
68
68
  "@fastify/static": "^9.0.0",
69
- "@fastify/swagger": "^9.6.1",
70
- "@fastify/swagger-ui": "^5.2.4",
71
- "@scalar/api-reference": "^1.43.1",
69
+ "@fastify/swagger": "^9.7.0",
70
+ "@fastify/swagger-ui": "^5.2.5",
71
+ "@scalar/api-reference": "^1.44.16",
72
72
  "detect-port": "^2.1.0",
73
- "fastify": "^5.6.2",
74
- "hookified": "^1.15.0",
73
+ "fastify": "^5.7.4",
74
+ "hookified": "^1.15.1",
75
75
  "html-escaper": "^3.0.3",
76
- "pino": "^10.1.1",
76
+ "pino": "^10.3.1",
77
77
  "pino-pretty": "^13.1.3"
78
78
  },
79
79
  "files": [