client-certificate-auth 1.1.3 → 1.3.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
@@ -7,7 +7,7 @@ Express/Connect middleware for client SSL certificate authentication (mTLS).
7
7
  [![codecov](https://codecov.io/gh/tgies/client-certificate-auth/graph/badge.svg)](https://codecov.io/gh/tgies/client-certificate-auth)
8
8
  [![stryker mutation testing](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftgies%2Fclient-certificate-auth%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/tgies/client-certificate-auth/master)
9
9
 
10
- 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~3,500 lines of test code for ~600 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
10
+ 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~4,477 lines of test code for ~679 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
11
11
 
12
12
  ## Installation
13
13
 
@@ -186,6 +186,112 @@ The `cert` parameter contains fields from [`tls.PeerCertificate`](https://nodejs
186
186
  - `valid_from`, `valid_to` - Validity period
187
187
  - `issuerCertificate` - Issuer's certificate (only when `includeChain: true`)
188
188
 
189
+ ### `extractClientCertificate(req, options?)`
190
+
191
+ Framework-agnostic certificate extraction function exported from `client-certificate-auth/extractor`. Use this when building adapters for non-Express frameworks or when you need certificate extraction without middleware.
192
+
193
+ **Parameters:**
194
+
195
+ | Name | Type | Description |
196
+ |------|------|-------------|
197
+ | `req` | `Object` | Request object with `headers` and optional `socket` |
198
+ | `req.headers` | `Record<string, string \| string[]>` | HTTP headers object |
199
+ | `req.socket` | `Object` | Optional TLS socket (for socket-based extraction) |
200
+ | `options` | `Object` | Same options as middleware (except `onAuthenticated`/`onRejected`) |
201
+
202
+ **Returns:** `ExtractionResult`
203
+
204
+ ```typescript
205
+ {
206
+ success: boolean;
207
+ certificate: PeerCertificate | null;
208
+ reason: string | null; // Rejection reason if success is false
209
+ }
210
+ ```
211
+
212
+ **Rejection reasons:**
213
+
214
+ - `'verification_header_mismatch'` - Proxy verify header didn't match expected value
215
+ - `'header_missing_or_malformed'` - Header extraction failed and no fallback configured
216
+ - `'socket_not_authorized'` - Socket not authorized for TLS client cert
217
+ - `'certificate_not_retrievable'` - Socket authorized but getPeerCertificate() returned empty
218
+
219
+ **Example - Building a Koa adapter:**
220
+
221
+ ```javascript
222
+ import { extractClientCertificate } from 'client-certificate-auth/extractor';
223
+
224
+ function koaClientCert(checkAuth, options = {}) {
225
+ return async (ctx, next) => {
226
+ const result = extractClientCertificate(ctx.req, options);
227
+
228
+ if (!result.success) {
229
+ ctx.throw(401, result.reason);
230
+ }
231
+
232
+ ctx.state.clientCertificate = result.certificate;
233
+
234
+ const allowed = await checkAuth(result.certificate, ctx.req);
235
+ if (!allowed) {
236
+ ctx.throw(401, 'Certificate not authorized');
237
+ }
238
+
239
+ await next();
240
+ };
241
+ }
242
+
243
+ // Usage
244
+ app.use(koaClientCert(
245
+ (cert) => cert.subject.CN === 'admin',
246
+ { certificateSource: 'aws-alb' }
247
+ ));
248
+ ```
249
+
250
+ **Example - Custom authentication flow:**
251
+
252
+ ```javascript
253
+ import { extractClientCertificate } from 'client-certificate-auth/extractor';
254
+
255
+ app.post('/api/login', (req, res) => {
256
+ // Extract certificate without middleware
257
+ const result = extractClientCertificate(req, {
258
+ certificateSource: 'envoy',
259
+ fallbackToSocket: true
260
+ });
261
+
262
+ if (!result.success) {
263
+ return res.status(401).json({ error: result.reason });
264
+ }
265
+
266
+ // Custom auth logic
267
+ const user = lookupUserByCertFingerprint(result.certificate.fingerprint);
268
+ if (!user) {
269
+ return res.status(403).json({ error: 'Certificate not registered' });
270
+ }
271
+
272
+ // Issue session token
273
+ const token = createSessionToken(user);
274
+ res.json({ token, user });
275
+ });
276
+ ```
277
+
278
+ ### Ecosystem
279
+
280
+ This package provides everything you need to build mTLS authentication for any Node.js framework:
281
+
282
+ - **Certificate extraction** via `extractClientCertificate()` - handles both socket and header-based extraction
283
+ - **Authorization helpers** - reusable validation callbacks for common patterns (`allowCN`, `allowFingerprints`, etc.)
284
+ - **Parser library** - decode certificates from various reverse proxy formats (Envoy XFCC, AWS ALB, Cloudflare, etc.)
285
+ - **Type definitions** - full TypeScript support
286
+
287
+ **Official framework adapters:**
288
+
289
+ - **[passport-client-certificate-auth](https://www.npmjs.com/package/passport-client-certificate-auth)** - Passport.js strategy for mTLS authentication
290
+
291
+ **Community adapters:**
292
+
293
+ If you build an adapter for another framework (Koa, Fastify, Hapi, NestJS, etc.), please open an issue or PR to get it listed here!
294
+
189
295
  ### Accessing the Certificate
190
296
 
191
297
  After authentication, the certificate is attached to `req.clientCertificate` for downstream handlers:
@@ -5,7 +5,7 @@
5
5
  * @license MIT
6
6
  */
7
7
 
8
- import { getCertificateFromHeaders } from './parsers.js';
8
+ import { extractClientCertificate } from './extractor.js';
9
9
 
10
10
  /**
11
11
  * @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('tls').TLSSocket & { authorized?: boolean; authorizationError?: string }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
@@ -113,72 +113,52 @@ export default function clientCertificateAuth(callback, options = {}) {
113
113
  });
114
114
  }
115
115
 
116
- // Determine if header-based extraction is configured
117
- const useHeaders = Boolean(certificateSource || certificateHeader);
118
-
119
116
  return function middleware(req, res, next) {
120
- let cert = null;
117
+ // Extract certificate using shared extractor logic
118
+ const result = extractClientCertificate(req, {
119
+ certificateSource,
120
+ certificateHeader,
121
+ headerEncoding,
122
+ fallbackToSocket,
123
+ includeChain,
124
+ verifyHeader,
125
+ verifyValue,
126
+ });
121
127
 
122
- // Try header-based extraction first if configured
123
- if (useHeaders) {
124
- // Verify upstream proxy's certificate validation if configured
125
- // Stryker disable next-line LogicalOperator: construction-time validation ensures both set or neither, so && vs || is equivalent
126
- if (verifyHeader && verifyValue) {
127
- const verifyStatus = req.headers[verifyHeader.toLowerCase()];
128
- if (Array.isArray(verifyStatus) || verifyStatus !== verifyValue) {
129
- safeCallHook(onRejected, null, req, 'verification_header_mismatch');
130
- const e = new Error(`Unauthorized: Certificate verification failed (${verifyStatus || 'header missing'})`);
131
- e.status = 401;
132
- return next(e);
133
- }
134
- }
135
- cert = getCertificateFromHeaders(req.headers, {
136
- certificateSource,
137
- certificateHeader,
138
- headerEncoding,
139
- });
128
+ if (!result.success) {
129
+ safeCallHook(onRejected, null, req, result.reason);
140
130
 
141
- // Normalize: strip chain unless includeChain is true
142
- if (cert && !includeChain && 'issuerCertificate' in cert) {
143
- delete cert.issuerCertificate;
144
- }
131
+ // Map rejection reasons to user-friendly error messages
132
+ let errorMessage;
133
+ let statusCode = 401;
145
134
 
146
- if (!cert) {
147
- // If no fallback, return 401 immediately
148
- if (!fallbackToSocket) {
149
- safeCallHook(onRejected, null, req, 'header_missing_or_malformed');
150
- const e = new Error('Unauthorized: Client certificate header missing or malformed');
151
- e.status = 401;
152
- return next(e);
135
+ switch (result.reason) {
136
+ case 'verification_header_mismatch': {
137
+ const verifyStatus = req.headers[verifyHeader.toLowerCase()];
138
+ errorMessage = `Unauthorized: Certificate verification failed (${verifyStatus || 'header missing'})`;
139
+ break;
153
140
  }
154
- }
155
- }
156
-
157
- // Fallback to socket-based extraction (original behavior)
158
- if (!cert) {
159
- // Ensure that the certificate was validated at the protocol level
160
- if (!req.socket?.authorized) {
161
- safeCallHook(onRejected, null, req, 'socket_not_authorized');
162
- const authError = req.socket?.authorizationError || 'unknown';
163
- const e = new Error(`Unauthorized: Client certificate required (${authError})`);
164
- e.status = 401;
165
- return next(e);
141
+ case 'header_missing_or_malformed':
142
+ errorMessage = 'Unauthorized: Client certificate header missing or malformed';
143
+ break;
144
+ case 'socket_not_authorized': {
145
+ const authError = req.socket?.authorizationError || 'unknown';
146
+ errorMessage = `Unauthorized: Client certificate required (${authError})`;
147
+ break;
148
+ }
149
+ case 'certificate_not_retrievable':
150
+ errorMessage = 'Client certificate was authenticated but certificate information could not be retrieved.';
151
+ statusCode = 500;
152
+ break;
166
153
  }
167
154
 
168
- // Obtain certificate details from socket
169
- cert = req.socket.getPeerCertificate(includeChain);
170
- if (!cert || Object.keys(cert).length === 0) {
171
- // Handle the bizarre case where a certificate was validated but we can't inspect it
172
- safeCallHook(onRejected, null, req, 'certificate_not_retrievable');
173
- const e = new Error(
174
- 'Client certificate was authenticated but certificate information could not be retrieved.'
175
- );
176
- e.status = 500;
177
- return next(e);
178
- }
155
+ const e = new Error(errorMessage);
156
+ e.status = statusCode;
157
+ return next(e);
179
158
  }
180
159
 
181
160
  // Attach certificate to request for downstream access
161
+ const cert = result.certificate;
182
162
  req.clientCertificate = cert;
183
163
 
184
164
  /**
@@ -197,9 +177,9 @@ export default function clientCertificateAuth(callback, options = {}) {
197
177
  }
198
178
 
199
179
  try {
200
- const result = callback(cert, req);
201
- if (result instanceof Promise) {
202
- result.then(doneAuthorizing).catch((err) => {
180
+ const callbackResult = callback(cert, req);
181
+ if (callbackResult instanceof Promise) {
182
+ callbackResult.then(doneAuthorizing).catch((err) => {
203
183
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
204
184
  if (err.status === undefined) {
205
185
  err.status = 401;
@@ -207,7 +187,7 @@ export default function clientCertificateAuth(callback, options = {}) {
207
187
  next(err);
208
188
  });
209
189
  } else {
210
- doneAuthorizing(result);
190
+ doneAuthorizing(callbackResult);
211
191
  }
212
192
  } catch (err) {
213
193
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * client-certificate-auth/extractor - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let _module;
10
+
11
+ async function load() {
12
+ // Stryker disable next-line ConditionalExpression,BlockStatement: test ordering caches _module from prior test; ConditionalExpression→true (always re-import) re-imports same module successfully; BlockStatement→{} uses cached value
13
+ if (!_module) {
14
+ // Stryker disable next-line StringLiteral: test ordering caches _module; StringLiteral→"" fails import but cached value masks it
15
+ _module = await import('./extractor.js');
16
+ }
17
+ return _module;
18
+ }
19
+
20
+ module.exports = { load };
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * client-certificate-auth/extractor - CommonJS type declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type * as ExtractorModule from './extractor.js';
8
+
9
+ declare const extractor: {
10
+ load(): Promise<typeof ExtractorModule>;
11
+ };
12
+
13
+ export = extractor;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @typedef {Object} ExtractionResult
3
+ * @property {boolean} success - Whether extraction succeeded
4
+ * @property {import('tls').PeerCertificate | null} certificate - Extracted certificate (null on failure)
5
+ * @property {string | null} reason - Rejection reason code (null on success)
6
+ *
7
+ * Rejection reasons:
8
+ * - 'verification_header_mismatch' - Proxy verify header didn't match expected value
9
+ * - 'header_missing_or_malformed' - Header extraction failed and no fallback configured
10
+ * - 'socket_not_authorized' - Socket not authorized for TLS client cert
11
+ * - 'certificate_not_retrievable' - Socket authorized but getPeerCertificate() returned empty
12
+ */
13
+ /**
14
+ * @typedef {Object} ExtractorOptions
15
+ * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Preset configuration
16
+ * @property {string} [certificateHeader] - Custom header name
17
+ * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] - Header encoding
18
+ * @property {boolean} [fallbackToSocket=false] - Try socket if header extraction fails
19
+ * @property {boolean} [includeChain=false] - Include issuerCertificate chain
20
+ * @property {string} [verifyHeader] - Header name for upstream verification status
21
+ * @property {string} [verifyValue] - Expected value for successful verification
22
+ */
23
+ /**
24
+ * Extract client certificate from request.
25
+ *
26
+ * Works with both header-based extraction (reverse proxy scenarios) and socket-based
27
+ * extraction (direct TLS connections). Returns a structured result object instead of
28
+ * throwing or using callbacks, making it suitable for any framework adapter.
29
+ *
30
+ * @param {Object} req - Request object with headers and optional socket
31
+ * @param {Record<string, string | string[] | undefined>} req.headers - HTTP headers object
32
+ * @param {Object} [req.socket] - TLS socket with getPeerCertificate() method
33
+ * @param {boolean} [req.socket.authorized] - Whether socket was authorized
34
+ * @param {(detailed: boolean) => import('tls').PeerCertificate} [req.socket.getPeerCertificate] - Get peer certificate
35
+ * @param {ExtractorOptions} [options={}] - Extraction options
36
+ * @returns {ExtractionResult}
37
+ *
38
+ * @example
39
+ * // AWS ALB header extraction
40
+ * const result = extractClientCertificate(req, { certificateSource: 'aws-alb' });
41
+ * if (result.success) {
42
+ * console.log('Certificate CN:', result.certificate.subject.CN);
43
+ * } else {
44
+ * console.error('Extraction failed:', result.reason);
45
+ * }
46
+ *
47
+ * @example
48
+ * // Socket extraction with fallback
49
+ * const result = extractClientCertificate(req, {
50
+ * certificateSource: 'envoy',
51
+ * fallbackToSocket: true
52
+ * });
53
+ */
54
+ export function extractClientCertificate(req: {
55
+ headers: Record<string, string | string[] | undefined>;
56
+ socket?: {
57
+ authorized?: boolean;
58
+ getPeerCertificate?: (detailed: boolean) => import("tls").PeerCertificate;
59
+ };
60
+ }, options?: ExtractorOptions): ExtractionResult;
61
+ export type ExtractionResult = {
62
+ /**
63
+ * - Whether extraction succeeded
64
+ */
65
+ success: boolean;
66
+ /**
67
+ * - Extracted certificate (null on failure)
68
+ */
69
+ certificate: import("tls").PeerCertificate | null;
70
+ /**
71
+ * - Rejection reason code (null on success)
72
+ *
73
+ * Rejection reasons:
74
+ * - 'verification_header_mismatch' - Proxy verify header didn't match expected value
75
+ * - 'header_missing_or_malformed' - Header extraction failed and no fallback configured
76
+ * - 'socket_not_authorized' - Socket not authorized for TLS client cert
77
+ * - 'certificate_not_retrievable' - Socket authorized but getPeerCertificate() returned empty
78
+ */
79
+ reason: string | null;
80
+ };
81
+ export type ExtractorOptions = {
82
+ /**
83
+ * - Preset configuration
84
+ */
85
+ certificateSource?: "aws-alb" | "envoy" | "cloudflare" | "traefik";
86
+ /**
87
+ * - Custom header name
88
+ */
89
+ certificateHeader?: string;
90
+ /**
91
+ * - Header encoding
92
+ */
93
+ headerEncoding?: "url-pem" | "url-pem-aws" | "xfcc" | "base64-der" | "rfc9440";
94
+ /**
95
+ * - Try socket if header extraction fails
96
+ */
97
+ fallbackToSocket?: boolean;
98
+ /**
99
+ * - Include issuerCertificate chain
100
+ */
101
+ includeChain?: boolean;
102
+ /**
103
+ * - Header name for upstream verification status
104
+ */
105
+ verifyHeader?: string;
106
+ /**
107
+ * - Expected value for successful verification
108
+ */
109
+ verifyValue?: string;
110
+ };
@@ -0,0 +1,160 @@
1
+ /*!
2
+ * client-certificate-auth - Certificate extraction module
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import { getCertificateFromHeaders } from './parsers.js';
8
+
9
+ /**
10
+ * @typedef {Object} ExtractionResult
11
+ * @property {boolean} success - Whether extraction succeeded
12
+ * @property {import('tls').PeerCertificate | null} certificate - Extracted certificate (null on failure)
13
+ * @property {string | null} reason - Rejection reason code (null on success)
14
+ *
15
+ * Rejection reasons:
16
+ * - 'verification_header_mismatch' - Proxy verify header didn't match expected value
17
+ * - 'header_missing_or_malformed' - Header extraction failed and no fallback configured
18
+ * - 'socket_not_authorized' - Socket not authorized for TLS client cert
19
+ * - 'certificate_not_retrievable' - Socket authorized but getPeerCertificate() returned empty
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} ExtractorOptions
24
+ * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Preset configuration
25
+ * @property {string} [certificateHeader] - Custom header name
26
+ * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] - Header encoding
27
+ * @property {boolean} [fallbackToSocket=false] - Try socket if header extraction fails
28
+ * @property {boolean} [includeChain=false] - Include issuerCertificate chain
29
+ * @property {string} [verifyHeader] - Header name for upstream verification status
30
+ * @property {string} [verifyValue] - Expected value for successful verification
31
+ */
32
+
33
+ /**
34
+ * Extract client certificate from request.
35
+ *
36
+ * Works with both header-based extraction (reverse proxy scenarios) and socket-based
37
+ * extraction (direct TLS connections). Returns a structured result object instead of
38
+ * throwing or using callbacks, making it suitable for any framework adapter.
39
+ *
40
+ * @param {Object} req - Request object with headers and optional socket
41
+ * @param {Record<string, string | string[] | undefined>} req.headers - HTTP headers object
42
+ * @param {Object} [req.socket] - TLS socket with getPeerCertificate() method
43
+ * @param {boolean} [req.socket.authorized] - Whether socket was authorized
44
+ * @param {(detailed: boolean) => import('tls').PeerCertificate} [req.socket.getPeerCertificate] - Get peer certificate
45
+ * @param {ExtractorOptions} [options={}] - Extraction options
46
+ * @returns {ExtractionResult}
47
+ *
48
+ * @example
49
+ * // AWS ALB header extraction
50
+ * const result = extractClientCertificate(req, { certificateSource: 'aws-alb' });
51
+ * if (result.success) {
52
+ * console.log('Certificate CN:', result.certificate.subject.CN);
53
+ * } else {
54
+ * console.error('Extraction failed:', result.reason);
55
+ * }
56
+ *
57
+ * @example
58
+ * // Socket extraction with fallback
59
+ * const result = extractClientCertificate(req, {
60
+ * certificateSource: 'envoy',
61
+ * fallbackToSocket: true
62
+ * });
63
+ */
64
+ export function extractClientCertificate(req, options = {}) {
65
+ const {
66
+ certificateSource,
67
+ certificateHeader,
68
+ headerEncoding,
69
+ fallbackToSocket = false,
70
+ includeChain = false,
71
+ verifyHeader,
72
+ verifyValue,
73
+ } = options;
74
+
75
+ // Validate verifyHeader/verifyValue pairing
76
+ if ((verifyHeader && !verifyValue) || (!verifyHeader && verifyValue)) {
77
+ throw new Error(
78
+ 'extractClientCertificate: verifyHeader and verifyValue must both be provided together, or both omitted'
79
+ );
80
+ }
81
+
82
+ const useHeaders = Boolean(certificateSource || certificateHeader);
83
+ let cert = null;
84
+
85
+ // Try header-based extraction first if configured
86
+ if (useHeaders) {
87
+ // Verify upstream proxy's certificate validation if configured
88
+ // Stryker disable next-line LogicalOperator: construction-time validation ensures both set or neither, single-side check is redundant but clearer
89
+ if (verifyHeader && verifyValue) {
90
+ const verifyStatus = req.headers[verifyHeader.toLowerCase()];
91
+ if (Array.isArray(verifyStatus) || verifyStatus !== verifyValue) {
92
+ return {
93
+ success: false,
94
+ certificate: null,
95
+ reason: 'verification_header_mismatch',
96
+ };
97
+ }
98
+ }
99
+
100
+ cert = getCertificateFromHeaders(req.headers, {
101
+ certificateSource,
102
+ certificateHeader,
103
+ headerEncoding,
104
+ });
105
+
106
+ // Normalize: strip chain unless includeChain is true
107
+ if (cert && !includeChain && 'issuerCertificate' in cert) {
108
+ delete cert.issuerCertificate;
109
+ }
110
+
111
+ if (!cert) {
112
+ // If no fallback, return error immediately
113
+ if (!fallbackToSocket) {
114
+ return {
115
+ success: false,
116
+ certificate: null,
117
+ reason: 'header_missing_or_malformed',
118
+ };
119
+ }
120
+ }
121
+ }
122
+
123
+ // Fallback to socket-based extraction (original behavior)
124
+ if (!cert) {
125
+ // Ensure that the certificate was validated at the protocol level
126
+ if (!req.socket?.authorized) {
127
+ return {
128
+ success: false,
129
+ certificate: null,
130
+ reason: 'socket_not_authorized',
131
+ };
132
+ }
133
+
134
+ // Obtain certificate details from socket
135
+ // TypeScript: cast to TLSSocket since we've validated this is a TLS connection
136
+ if (typeof req.socket.getPeerCertificate !== 'function') {
137
+ return {
138
+ success: false,
139
+ certificate: null,
140
+ reason: 'certificate_not_retrievable',
141
+ };
142
+ }
143
+
144
+ cert = /** @type {import('tls').TLSSocket} */ (req.socket).getPeerCertificate(includeChain);
145
+ if (!cert || Object.keys(cert).length === 0) {
146
+ // Handle the case where a certificate was validated but we can't inspect it
147
+ return {
148
+ success: false,
149
+ certificate: null,
150
+ reason: 'certificate_not_retrievable',
151
+ };
152
+ }
153
+ }
154
+
155
+ return {
156
+ success: true,
157
+ certificate: cert,
158
+ reason: null,
159
+ };
160
+ }
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * client-certificate-auth/helpers - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let _module;
10
+
11
+ async function load() {
12
+ // Stryker disable next-line ConditionalExpression,BlockStatement: test ordering caches _module from prior test; ConditionalExpression→true (always re-import) re-imports same module successfully; BlockStatement→{} uses cached value
13
+ if (!_module) {
14
+ // Stryker disable next-line StringLiteral: test ordering caches _module; StringLiteral→"" fails import but cached value masks it
15
+ _module = await import('./helpers.js');
16
+ }
17
+ return _module;
18
+ }
19
+
20
+ module.exports = { load };
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * client-certificate-auth/helpers - CommonJS type declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type * as HelpersModule from './helpers.js';
8
+
9
+ declare const helpers: {
10
+ load(): Promise<typeof HelpersModule>;
11
+ };
12
+
13
+ export = helpers;
package/lib/helpers.d.ts CHANGED
@@ -4,13 +4,9 @@
4
4
  * @license MIT
5
5
  */
6
6
 
7
- import type { PeerCertificate, DetailedPeerCertificate } from 'tls';
8
- import type { ClientCertRequest } from './clientCertificateAuth.js';
7
+ import type { ValidationCallback } from './clientCertificateAuth.js';
9
8
 
10
- /**
11
- * Validation callback for clientCertificateAuth middleware.
12
- */
13
- export type ValidationCallback = (cert: PeerCertificate | DetailedPeerCertificate, req?: ClientCertRequest) => boolean | Promise<boolean>;
9
+ export type { ValidationCallback };
14
10
 
15
11
  /**
16
12
  * Distinguished Name fields for matching.
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * client-certificate-auth/parsers - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let _module;
10
+
11
+ async function load() {
12
+ // Stryker disable next-line ConditionalExpression,BlockStatement: test ordering caches _module from prior test; ConditionalExpression→true (always re-import) re-imports same module successfully; BlockStatement→{} uses cached value
13
+ if (!_module) {
14
+ // Stryker disable next-line StringLiteral: test ordering caches _module; StringLiteral→"" fails import but cached value masks it
15
+ _module = await import('./parsers.js');
16
+ }
17
+ return _module;
18
+ }
19
+
20
+ module.exports = { load };
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * client-certificate-auth/parsers - CommonJS type declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type * as ParsersModule from './parsers.js';
8
+
9
+ declare const parsers: {
10
+ load(): Promise<typeof ParsersModule>;
11
+ };
12
+
13
+ export = parsers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "Express/Connect middleware for mTLS client certificate authentication with reverse proxy support (AWS ALB, Envoy, Cloudflare, Traefik)",
5
5
  "homepage": "https://github.com/tgies/client-certificate-auth",
6
6
  "bugs": {
@@ -26,35 +26,66 @@
26
26
  "import": {
27
27
  "types": "./lib/parsers.d.ts",
28
28
  "default": "./lib/parsers.js"
29
+ },
30
+ "require": {
31
+ "types": "./lib/parsers.d.cts",
32
+ "default": "./lib/parsers.cjs"
29
33
  }
30
34
  },
31
35
  "./helpers": {
32
36
  "import": {
33
37
  "types": "./lib/helpers.d.ts",
34
38
  "default": "./lib/helpers.js"
39
+ },
40
+ "require": {
41
+ "types": "./lib/helpers.d.cts",
42
+ "default": "./lib/helpers.cjs"
43
+ }
44
+ },
45
+ "./extractor": {
46
+ "import": {
47
+ "types": "./lib/extractor.d.ts",
48
+ "default": "./lib/extractor.js"
49
+ },
50
+ "require": {
51
+ "types": "./lib/extractor.d.cts",
52
+ "default": "./lib/extractor.cjs"
35
53
  }
36
54
  }
37
55
  },
38
56
  "main": "./lib/clientCertificateAuth.cjs",
39
57
  "types": "./lib/clientCertificateAuth.d.ts",
58
+ "typesVersions": {
59
+ "*": {
60
+ "parsers": [
61
+ "./lib/parsers.d.ts"
62
+ ],
63
+ "helpers": [
64
+ "./lib/helpers.d.ts"
65
+ ],
66
+ "extractor": [
67
+ "./lib/extractor.d.ts"
68
+ ]
69
+ }
70
+ },
40
71
  "engines": {
41
72
  "node": ">= 20"
42
73
  },
43
74
  "devDependencies": {
44
75
  "@commitlint/cli": "^20.4.1",
45
76
  "@commitlint/config-conventional": "^20.4.1",
46
- "@stryker-mutator/core": "^9.4.0",
47
- "@stryker-mutator/jest-runner": "^9.4.0",
48
- "@types/express": "^5.0.0",
49
- "@types/node": "^25.2.1",
77
+ "@stryker-mutator/core": "^9.5.1",
78
+ "@stryker-mutator/jest-runner": "^9.5.1",
79
+ "@types/express": "^5.0.6",
80
+ "@types/node": "^25.2.3",
50
81
  "c8": "^10.1.3",
51
82
  "eslint": "^9.17.0",
52
- "globals": "^17.0.0",
83
+ "globals": "^17.3.0",
53
84
  "husky": "^9.1.7",
54
85
  "jest": "^30.2.0",
55
86
  "lint-staged": "^16.2.7",
56
- "selfsigned": "^5.4.0",
57
- "typescript": "^5.7.2",
87
+ "selfsigned": "^5.5.0",
88
+ "typescript": "^5.9.3",
58
89
  "ws": "^8.19.0"
59
90
  },
60
91
  "directories": {