0http-bun 1.1.3 → 1.2.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 +72 -3
- package/common.d.ts +37 -2
- package/lib/middleware/README.md +870 -0
- package/lib/middleware/body-parser.js +783 -0
- package/lib/middleware/cors.js +225 -0
- package/lib/middleware/index.d.ts +236 -0
- package/lib/middleware/index.js +45 -0
- package/lib/middleware/jwt-auth.js +406 -0
- package/lib/middleware/logger.js +310 -0
- package/lib/middleware/rate-limit.js +270 -0
- package/lib/router/sequential.js +10 -2
- package/package.json +6 -3
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
const {jwtVerify, createRemoteJWKSet, errors} = require('jose')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates JWT authentication middleware
|
|
5
|
+
* @param {Object} options - JWT configuration options
|
|
6
|
+
* @param {string|Uint8Array|Function} options.secret - JWT secret or key getter function
|
|
7
|
+
* @param {string} options.jwksUri - JWKS URI for remote key verification
|
|
8
|
+
* @param {Object} options.jwtOptions - Additional JWT verification options
|
|
9
|
+
* @param {Function} options.getToken - Custom token extraction function
|
|
10
|
+
* @param {string} options.tokenHeader - Custom header for token extraction
|
|
11
|
+
* @param {string} options.tokenQuery - Query parameter name for token extraction
|
|
12
|
+
* @param {boolean} options.optional - Whether authentication is optional
|
|
13
|
+
* @param {Array<string>} options.excludePaths - Paths to exclude from authentication
|
|
14
|
+
* @param {Array<string>|Function} options.apiKeys - Valid API keys for API key authentication
|
|
15
|
+
* @param {string} options.apiKeyHeader - Header name for API key
|
|
16
|
+
* @param {Function} options.apiKeyValidator - Custom API key validation function
|
|
17
|
+
* @param {Function} options.unauthorizedResponse - Custom unauthorized response generator
|
|
18
|
+
* @param {Function} options.onError - Custom error handler
|
|
19
|
+
* @param {string|Array<string>} options.audience - Expected JWT audience
|
|
20
|
+
* @param {string} options.issuer - Expected JWT issuer
|
|
21
|
+
* @returns {Function} Middleware function
|
|
22
|
+
*/
|
|
23
|
+
function createJWTAuth(options = {}) {
|
|
24
|
+
const {
|
|
25
|
+
secret,
|
|
26
|
+
jwksUri,
|
|
27
|
+
jwks,
|
|
28
|
+
jwtOptions = {},
|
|
29
|
+
getToken,
|
|
30
|
+
tokenHeader,
|
|
31
|
+
tokenQuery,
|
|
32
|
+
optional = false,
|
|
33
|
+
excludePaths = [],
|
|
34
|
+
apiKeys,
|
|
35
|
+
apiKeyHeader = 'x-api-key',
|
|
36
|
+
apiKeyValidator,
|
|
37
|
+
validateApiKey,
|
|
38
|
+
unauthorizedResponse,
|
|
39
|
+
onError,
|
|
40
|
+
audience,
|
|
41
|
+
issuer,
|
|
42
|
+
algorithms,
|
|
43
|
+
} = options
|
|
44
|
+
|
|
45
|
+
// API key mode doesn't require JWT secret
|
|
46
|
+
const hasApiKeyMode = apiKeys || apiKeyValidator || validateApiKey
|
|
47
|
+
if (!secret && !jwksUri && !jwks && !hasApiKeyMode) {
|
|
48
|
+
throw new Error('JWT middleware requires either secret or jwksUri')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Setup key resolver for JWT
|
|
52
|
+
let keyLike
|
|
53
|
+
if (jwks) {
|
|
54
|
+
// If jwks is a mock or custom resolver with getKey method
|
|
55
|
+
if (typeof jwks.getKey === 'function') {
|
|
56
|
+
keyLike = async (protectedHeader, token) => {
|
|
57
|
+
return jwks.getKey(protectedHeader, token)
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
keyLike = jwks
|
|
61
|
+
}
|
|
62
|
+
} else if (jwksUri) {
|
|
63
|
+
keyLike = createRemoteJWKSet(new URL(jwksUri))
|
|
64
|
+
} else if (typeof secret === 'function') {
|
|
65
|
+
keyLike = secret
|
|
66
|
+
} else {
|
|
67
|
+
keyLike = secret
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Default JWT verification options
|
|
71
|
+
const defaultJwtOptions = {
|
|
72
|
+
algorithms: algorithms || ['HS256', 'RS256'],
|
|
73
|
+
audience,
|
|
74
|
+
issuer,
|
|
75
|
+
...jwtOptions,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return async function jwtAuthMiddleware(req, next) {
|
|
79
|
+
const url = new URL(req.url)
|
|
80
|
+
|
|
81
|
+
// Skip authentication for excluded paths
|
|
82
|
+
if (excludePaths.some((path) => url.pathname.startsWith(path))) {
|
|
83
|
+
return next()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Try API key authentication first if configured
|
|
88
|
+
if (hasApiKeyMode) {
|
|
89
|
+
const apiKey = req.headers.get(apiKeyHeader)
|
|
90
|
+
if (apiKey) {
|
|
91
|
+
const validationResult = await validateApiKeyInternal(
|
|
92
|
+
apiKey,
|
|
93
|
+
apiKeys,
|
|
94
|
+
apiKeyValidator || validateApiKey,
|
|
95
|
+
req,
|
|
96
|
+
)
|
|
97
|
+
if (validationResult !== false) {
|
|
98
|
+
// Set API key context
|
|
99
|
+
req.ctx = req.ctx || {}
|
|
100
|
+
req.ctx.apiKey = apiKey
|
|
101
|
+
|
|
102
|
+
// If validation result is an object, use it as user data, otherwise default
|
|
103
|
+
const userData =
|
|
104
|
+
validationResult && typeof validationResult === 'object'
|
|
105
|
+
? validationResult
|
|
106
|
+
: {apiKey}
|
|
107
|
+
|
|
108
|
+
req.ctx.user = userData
|
|
109
|
+
req.apiKey = apiKey
|
|
110
|
+
req.user = userData
|
|
111
|
+
return next()
|
|
112
|
+
} else {
|
|
113
|
+
return handleAuthError(
|
|
114
|
+
new Error('Invalid API key'),
|
|
115
|
+
{unauthorizedResponse, onError},
|
|
116
|
+
req,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Extract JWT token from request
|
|
123
|
+
const token = extractToken(req, {getToken, tokenHeader, tokenQuery})
|
|
124
|
+
|
|
125
|
+
if (!token) {
|
|
126
|
+
if (optional) {
|
|
127
|
+
return next()
|
|
128
|
+
}
|
|
129
|
+
return handleAuthError(
|
|
130
|
+
new Error('Authentication required'),
|
|
131
|
+
{unauthorizedResponse, onError},
|
|
132
|
+
req,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Only verify JWT if we have JWT configuration
|
|
137
|
+
if (!keyLike) {
|
|
138
|
+
return handleAuthError(
|
|
139
|
+
new Error('JWT verification not configured'),
|
|
140
|
+
{unauthorizedResponse, onError},
|
|
141
|
+
req,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Verify JWT token
|
|
146
|
+
const {payload, protectedHeader} = await jwtVerify(
|
|
147
|
+
token,
|
|
148
|
+
keyLike,
|
|
149
|
+
defaultJwtOptions,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Add user info to request context
|
|
153
|
+
req.ctx = req.ctx || {}
|
|
154
|
+
req.ctx.user = payload
|
|
155
|
+
req.ctx.jwt = {
|
|
156
|
+
payload,
|
|
157
|
+
header: protectedHeader,
|
|
158
|
+
token,
|
|
159
|
+
}
|
|
160
|
+
req.user = payload // Mirror to root for compatibility
|
|
161
|
+
req.jwt = {
|
|
162
|
+
payload,
|
|
163
|
+
header: protectedHeader,
|
|
164
|
+
token,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return next()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (optional && (!hasApiKeyMode || !req.headers.get(apiKeyHeader))) {
|
|
170
|
+
return next()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return handleAuthError(error, {unauthorizedResponse, onError}, req)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validates API key
|
|
180
|
+
* @param {string} apiKey - API key to validate
|
|
181
|
+
* @param {Array<string>|Function} apiKeys - Valid API keys or validator function
|
|
182
|
+
* @param {Function} apiKeyValidator - Custom validator function
|
|
183
|
+
* @param {Request} req - Request object
|
|
184
|
+
* @returns {boolean|Object} Whether API key is valid or user object
|
|
185
|
+
*/
|
|
186
|
+
async function validateApiKeyInternal(apiKey, apiKeys, apiKeyValidator, req) {
|
|
187
|
+
if (apiKeyValidator) {
|
|
188
|
+
// Check if this is the simplified validateApiKey function (expects only key)
|
|
189
|
+
if (apiKeyValidator.length === 1) {
|
|
190
|
+
const result = await apiKeyValidator(apiKey)
|
|
191
|
+
return result || false
|
|
192
|
+
}
|
|
193
|
+
// Otherwise call with both key and req
|
|
194
|
+
const result = await apiKeyValidator(apiKey, req)
|
|
195
|
+
return result || false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof apiKeys === 'function') {
|
|
199
|
+
const result = await apiKeys(apiKey, req)
|
|
200
|
+
return result || false
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(apiKeys)) {
|
|
204
|
+
return apiKeys.includes(apiKey)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return apiKeys === apiKey
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extracts JWT token from request
|
|
212
|
+
* @param {Request} req - Request object
|
|
213
|
+
* @param {Object} options - Extraction options
|
|
214
|
+
* @returns {string|null} JWT token or null if not found
|
|
215
|
+
*/
|
|
216
|
+
function extractToken(req, options = {}) {
|
|
217
|
+
const {getToken, tokenHeader, tokenQuery} = options
|
|
218
|
+
|
|
219
|
+
// Use custom token extractor if provided
|
|
220
|
+
if (getToken) {
|
|
221
|
+
return getToken(req)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Try custom header
|
|
225
|
+
if (tokenHeader) {
|
|
226
|
+
const token = req.headers.get(tokenHeader)
|
|
227
|
+
if (token) return token
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Try query parameter
|
|
231
|
+
if (tokenQuery) {
|
|
232
|
+
const url = new URL(req.url)
|
|
233
|
+
const token = url.searchParams.get(tokenQuery)
|
|
234
|
+
if (token) return token
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default: Authorization header
|
|
238
|
+
return extractTokenFromHeader(req)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handles authentication errors
|
|
243
|
+
* @param {Error} error - Authentication error
|
|
244
|
+
* @param {Object} handlers - Error handling functions
|
|
245
|
+
* @param {Request} req - Request object
|
|
246
|
+
* @returns {Response} Error response
|
|
247
|
+
*/
|
|
248
|
+
function handleAuthError(error, handlers = {}, req) {
|
|
249
|
+
const {unauthorizedResponse, onError} = handlers
|
|
250
|
+
|
|
251
|
+
// Call custom error handler if provided
|
|
252
|
+
if (onError) {
|
|
253
|
+
try {
|
|
254
|
+
const result = onError(error, req)
|
|
255
|
+
if (result instanceof Response) {
|
|
256
|
+
return result
|
|
257
|
+
}
|
|
258
|
+
} catch (handlerError) {
|
|
259
|
+
// Fall back to default handling if custom handler fails
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Use custom unauthorized response if provided
|
|
264
|
+
if (unauthorizedResponse) {
|
|
265
|
+
try {
|
|
266
|
+
// If it's already a Response object, return it directly
|
|
267
|
+
if (unauthorizedResponse instanceof Response) {
|
|
268
|
+
return unauthorizedResponse
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If it's a function, call it
|
|
272
|
+
if (typeof unauthorizedResponse === 'function') {
|
|
273
|
+
const response = unauthorizedResponse(error, req)
|
|
274
|
+
if (response instanceof Response) {
|
|
275
|
+
return response
|
|
276
|
+
}
|
|
277
|
+
// If not a Response object, treat as response data
|
|
278
|
+
if (response && typeof response === 'object') {
|
|
279
|
+
return new Response(
|
|
280
|
+
typeof response.body === 'string'
|
|
281
|
+
? response.body
|
|
282
|
+
: JSON.stringify(response.body || response),
|
|
283
|
+
{
|
|
284
|
+
status: response.status || 401,
|
|
285
|
+
headers: response.headers || {'Content-Type': 'application/json'},
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (responseError) {
|
|
291
|
+
// Fall back to default response if custom response fails
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Default error handling
|
|
296
|
+
let statusCode = 401
|
|
297
|
+
let message = 'Invalid token'
|
|
298
|
+
|
|
299
|
+
if (error.message === 'Authentication required') {
|
|
300
|
+
message = 'Authentication required'
|
|
301
|
+
} else if (error.message === 'Invalid API key') {
|
|
302
|
+
message = 'Invalid API key'
|
|
303
|
+
} else if (error.message === 'JWT verification not configured') {
|
|
304
|
+
message = 'JWT verification not configured'
|
|
305
|
+
} else if (error instanceof errors.JWTExpired) {
|
|
306
|
+
message = 'Token expired'
|
|
307
|
+
} else if (error instanceof errors.JWTInvalid) {
|
|
308
|
+
message = 'Invalid token format'
|
|
309
|
+
} else if (error instanceof errors.JWKSNoMatchingKey) {
|
|
310
|
+
message = 'Token signature verification failed'
|
|
311
|
+
} else if (error.message.includes('audience')) {
|
|
312
|
+
message = 'Invalid token audience'
|
|
313
|
+
} else if (error.message.includes('issuer')) {
|
|
314
|
+
message = 'Invalid token issuer'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return new Response(JSON.stringify({error: message}), {
|
|
318
|
+
status: statusCode,
|
|
319
|
+
headers: {'Content-Type': 'application/json'},
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Extracts JWT token from Authorization header
|
|
325
|
+
* @param {Request} req - Request object
|
|
326
|
+
* @returns {string|null} JWT token or null if not found
|
|
327
|
+
*/
|
|
328
|
+
function extractTokenFromHeader(req) {
|
|
329
|
+
const authorization = req.headers.get('authorization')
|
|
330
|
+
|
|
331
|
+
if (!authorization) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const parts = authorization.split(' ')
|
|
336
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return parts[1]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Creates a simple API key authentication middleware
|
|
345
|
+
* @param {Object} options - API key configuration
|
|
346
|
+
* @param {string|Array<string>|Function} options.keys - Valid API keys or validation function
|
|
347
|
+
* @param {string} options.header - Header name for API key (default: 'x-api-key')
|
|
348
|
+
* @param {Function} options.getKey - Custom key extraction function
|
|
349
|
+
* @returns {Function} Middleware function
|
|
350
|
+
*/
|
|
351
|
+
function createAPIKeyAuth(options = {}) {
|
|
352
|
+
const {keys, header = 'x-api-key', getKey} = options
|
|
353
|
+
|
|
354
|
+
if (!keys) {
|
|
355
|
+
throw new Error('API key middleware requires keys configuration')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const validateKey =
|
|
359
|
+
typeof keys === 'function'
|
|
360
|
+
? keys
|
|
361
|
+
: (key) => (Array.isArray(keys) ? keys.includes(key) : keys === key)
|
|
362
|
+
|
|
363
|
+
return async function apiKeyAuthMiddleware(req, next) {
|
|
364
|
+
try {
|
|
365
|
+
// Extract API key
|
|
366
|
+
const apiKey = getKey ? getKey(req) : req.headers.get(header)
|
|
367
|
+
|
|
368
|
+
if (!apiKey) {
|
|
369
|
+
return new Response(JSON.stringify({error: 'API key required'}), {
|
|
370
|
+
status: 401,
|
|
371
|
+
headers: {'Content-Type': 'application/json'},
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Validate API key
|
|
376
|
+
const isValid = await validateKey(apiKey, req)
|
|
377
|
+
|
|
378
|
+
if (!isValid) {
|
|
379
|
+
return new Response(JSON.stringify({error: 'Invalid API key'}), {
|
|
380
|
+
status: 401,
|
|
381
|
+
headers: {'Content-Type': 'application/json'},
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add API key info to context
|
|
386
|
+
req.ctx = req.ctx || {}
|
|
387
|
+
req.ctx.apiKey = apiKey
|
|
388
|
+
|
|
389
|
+
return next()
|
|
390
|
+
} catch (error) {
|
|
391
|
+
return new Response(JSON.stringify({error: 'Authentication failed'}), {
|
|
392
|
+
status: 500,
|
|
393
|
+
headers: {'Content-Type': 'application/json'},
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
createJWTAuth,
|
|
401
|
+
createAPIKeyAuth,
|
|
402
|
+
extractTokenFromHeader,
|
|
403
|
+
extractToken,
|
|
404
|
+
validateApiKeyInternal,
|
|
405
|
+
handleAuthError,
|
|
406
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
const pino = require('pino')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a logging middleware using Pino logger
|
|
6
|
+
* @param {Object} options - Logger configuration options
|
|
7
|
+
* @param {Object} options.pinoOptions - Pino logger options
|
|
8
|
+
* @param {Function} options.serializers - Custom serializers for request/response
|
|
9
|
+
* @param {boolean} options.logBody - Whether to log request/response bodies
|
|
10
|
+
* @param {Array<string>} options.excludePaths - Paths to exclude from logging
|
|
11
|
+
* @param {Object} options.logger - Injected logger instance
|
|
12
|
+
* @param {string} options.level - Log level (alternative to pinoOptions.level)
|
|
13
|
+
* @param {string} options.requestIdHeader - Header name to read request ID from
|
|
14
|
+
* @param {Function} options.generateRequestId - Custom request ID generator function
|
|
15
|
+
* @returns {Function} Middleware function
|
|
16
|
+
*/
|
|
17
|
+
function createLogger(options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
pinoOptions = {},
|
|
20
|
+
logBody = false,
|
|
21
|
+
excludePaths = ['/health', '/ping', '/favicon.ico'],
|
|
22
|
+
logger: injectedLogger,
|
|
23
|
+
level,
|
|
24
|
+
serializers,
|
|
25
|
+
requestIdHeader,
|
|
26
|
+
generateRequestId,
|
|
27
|
+
} = options
|
|
28
|
+
|
|
29
|
+
// Build final pino options with proper precedence
|
|
30
|
+
const finalPinoOptions = {
|
|
31
|
+
level: level || pinoOptions.level || process.env.LOG_LEVEL || 'info',
|
|
32
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
33
|
+
formatters: {
|
|
34
|
+
level: (label) => ({level: label.toUpperCase()}),
|
|
35
|
+
},
|
|
36
|
+
serializers: {
|
|
37
|
+
req: (req) => ({
|
|
38
|
+
method: req.method,
|
|
39
|
+
url: req.url,
|
|
40
|
+
headers: req.headers,
|
|
41
|
+
...(logBody && req.body ? {body: req.body} : {}),
|
|
42
|
+
}),
|
|
43
|
+
// Default res serializer removed to allow logResponse to handle it fully
|
|
44
|
+
err: pino.stdSerializers.err,
|
|
45
|
+
// Merge in custom serializers if provided
|
|
46
|
+
...(serializers || {}),
|
|
47
|
+
},
|
|
48
|
+
...pinoOptions,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Use injected logger if provided (for tests), otherwise create a new one
|
|
52
|
+
const logger = injectedLogger || pino(finalPinoOptions)
|
|
53
|
+
|
|
54
|
+
return function loggerMiddleware(req, next) {
|
|
55
|
+
const startTime = process.hrtime.bigint()
|
|
56
|
+
const url = new URL(req.url)
|
|
57
|
+
|
|
58
|
+
if (excludePaths.some((path) => url.pathname.startsWith(path))) {
|
|
59
|
+
return next()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Generate or extract request ID
|
|
63
|
+
let requestId
|
|
64
|
+
if (requestIdHeader && req.headers.get(requestIdHeader)) {
|
|
65
|
+
requestId = req.headers.get(requestIdHeader)
|
|
66
|
+
} else if (generateRequestId) {
|
|
67
|
+
requestId = generateRequestId()
|
|
68
|
+
} else {
|
|
69
|
+
requestId = crypto.randomUUID()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add logger and requestId to context and root
|
|
73
|
+
req.ctx = req.ctx || {}
|
|
74
|
+
req.ctx.requestId = requestId
|
|
75
|
+
req.requestId = requestId
|
|
76
|
+
|
|
77
|
+
// Create child logger with request context
|
|
78
|
+
const childLogger = logger.child({
|
|
79
|
+
requestId: requestId,
|
|
80
|
+
method: req.method,
|
|
81
|
+
path: url.pathname,
|
|
82
|
+
})
|
|
83
|
+
req.ctx.log = childLogger
|
|
84
|
+
req.log = childLogger
|
|
85
|
+
|
|
86
|
+
// Check if we should log based on level
|
|
87
|
+
const effectiveLevel =
|
|
88
|
+
level || pinoOptions.level || process.env.LOG_LEVEL || 'info'
|
|
89
|
+
const shouldLogInfo = shouldLog('info', effectiveLevel)
|
|
90
|
+
|
|
91
|
+
// Log request started
|
|
92
|
+
if (shouldLogInfo) {
|
|
93
|
+
const logObj = {
|
|
94
|
+
msg: 'Request started',
|
|
95
|
+
method: req.method,
|
|
96
|
+
url: url.pathname,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Apply custom serializers if provided
|
|
100
|
+
if (serializers && serializers.req) {
|
|
101
|
+
Object.assign(logObj, serializers.req(req))
|
|
102
|
+
} else if (logBody && req.body) {
|
|
103
|
+
// Add body to log if logBody is enabled and no custom serializer
|
|
104
|
+
logObj.body = req.body
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
childLogger.info(logObj)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = next()
|
|
112
|
+
if (result instanceof Promise) {
|
|
113
|
+
return result
|
|
114
|
+
.then((response) => {
|
|
115
|
+
logResponse(
|
|
116
|
+
childLogger,
|
|
117
|
+
response,
|
|
118
|
+
startTime,
|
|
119
|
+
req,
|
|
120
|
+
url,
|
|
121
|
+
shouldLogInfo,
|
|
122
|
+
serializers,
|
|
123
|
+
logBody,
|
|
124
|
+
)
|
|
125
|
+
return response
|
|
126
|
+
})
|
|
127
|
+
.catch((error) => {
|
|
128
|
+
logError(childLogger, error, startTime)
|
|
129
|
+
throw error
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
logResponse(
|
|
133
|
+
childLogger,
|
|
134
|
+
result,
|
|
135
|
+
startTime,
|
|
136
|
+
req,
|
|
137
|
+
url,
|
|
138
|
+
shouldLogInfo,
|
|
139
|
+
serializers,
|
|
140
|
+
logBody,
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logError(childLogger, error, startTime)
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Helper function to determine if we should log at a given level
|
|
152
|
+
function shouldLog(logLevel, configuredLevel) {
|
|
153
|
+
const levels = {
|
|
154
|
+
trace: 10,
|
|
155
|
+
debug: 20,
|
|
156
|
+
info: 30,
|
|
157
|
+
warn: 40,
|
|
158
|
+
error: 50,
|
|
159
|
+
fatal: 60,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const logLevelNum = levels[logLevel] || 30
|
|
163
|
+
const configuredLevelNum = levels[configuredLevel] || 30
|
|
164
|
+
|
|
165
|
+
return logLevelNum >= configuredLevelNum
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function logResponse(
|
|
169
|
+
logger,
|
|
170
|
+
response,
|
|
171
|
+
startTime,
|
|
172
|
+
req,
|
|
173
|
+
url,
|
|
174
|
+
shouldLogInfo,
|
|
175
|
+
customSerializers, // serializers from createLogger options
|
|
176
|
+
logBodyOpt, // logBody from createLogger options
|
|
177
|
+
) {
|
|
178
|
+
if (!shouldLogInfo) return
|
|
179
|
+
|
|
180
|
+
const duration = Number(process.hrtime.bigint() - startTime) / 1000000
|
|
181
|
+
|
|
182
|
+
let responseSize
|
|
183
|
+
if (response) {
|
|
184
|
+
if (response.headers && response.headers.get) {
|
|
185
|
+
const contentLength = response.headers.get('content-length')
|
|
186
|
+
if (contentLength) {
|
|
187
|
+
responseSize = parseInt(contentLength, 10)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (responseSize === undefined) {
|
|
192
|
+
const bodyToMeasure = response.hasOwnProperty('_bodyForLogger')
|
|
193
|
+
? response._bodyForLogger
|
|
194
|
+
: response.body
|
|
195
|
+
if (bodyToMeasure instanceof ReadableStream) {
|
|
196
|
+
responseSize = undefined
|
|
197
|
+
} else if (typeof bodyToMeasure === 'string') {
|
|
198
|
+
responseSize = Buffer.byteLength(bodyToMeasure, 'utf8')
|
|
199
|
+
} else if (bodyToMeasure instanceof ArrayBuffer) {
|
|
200
|
+
responseSize = bodyToMeasure.byteLength
|
|
201
|
+
} else if (bodyToMeasure instanceof Uint8Array) {
|
|
202
|
+
responseSize = bodyToMeasure.length
|
|
203
|
+
} else if (bodyToMeasure === null || bodyToMeasure === undefined) {
|
|
204
|
+
responseSize = 0
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const logEntry = {
|
|
210
|
+
msg: 'Request completed',
|
|
211
|
+
method: req.method,
|
|
212
|
+
url: url.pathname,
|
|
213
|
+
status: response && response.status,
|
|
214
|
+
duration: duration,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (responseSize !== undefined) {
|
|
218
|
+
logEntry.responseSize = responseSize
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle response serialization
|
|
222
|
+
if (customSerializers && customSerializers.res) {
|
|
223
|
+
// Custom serializer is responsible for all response fields it wants to log
|
|
224
|
+
const serializedRes = customSerializers.res(response)
|
|
225
|
+
Object.assign(logEntry, serializedRes)
|
|
226
|
+
} else {
|
|
227
|
+
// No custom res serializer: default handling for headers
|
|
228
|
+
if (
|
|
229
|
+
response &&
|
|
230
|
+
response.headers &&
|
|
231
|
+
typeof response.headers.entries === 'function'
|
|
232
|
+
) {
|
|
233
|
+
logEntry.headers = Object.fromEntries(response.headers.entries())
|
|
234
|
+
} else if (response && response.headers) {
|
|
235
|
+
logEntry.headers = response.headers
|
|
236
|
+
} else {
|
|
237
|
+
logEntry.headers = {}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If logBodyOpt is true and body wasn't added by a custom serializer (or no custom serializer)
|
|
242
|
+
if (logBodyOpt && response && !logEntry.hasOwnProperty('body')) {
|
|
243
|
+
logEntry.body = response.hasOwnProperty('_bodyForLogger')
|
|
244
|
+
? response._bodyForLogger
|
|
245
|
+
: response.body
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logger.info(logEntry)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function logError(logger, error, startTime) {
|
|
252
|
+
const duration = Number(process.hrtime.bigint() - startTime) / 1000000
|
|
253
|
+
logger.error({
|
|
254
|
+
msg: 'Request failed',
|
|
255
|
+
error: error && error.message,
|
|
256
|
+
duration: duration,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Simple request logger for development
|
|
262
|
+
* @returns {Function} Middleware function
|
|
263
|
+
*/
|
|
264
|
+
function simpleLogger() {
|
|
265
|
+
return function simpleLoggerMiddleware(req, next) {
|
|
266
|
+
const startTime = Date.now()
|
|
267
|
+
const method = req.method
|
|
268
|
+
const url = new URL(req.url)
|
|
269
|
+
const pathname = url.pathname
|
|
270
|
+
|
|
271
|
+
console.log(`→ ${method} ${pathname}`)
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const result = next()
|
|
275
|
+
|
|
276
|
+
if (result instanceof Promise) {
|
|
277
|
+
return result
|
|
278
|
+
.then((response) => {
|
|
279
|
+
const duration = Date.now() - startTime
|
|
280
|
+
console.log(
|
|
281
|
+
`← ${method} ${pathname} ${response.status} (${duration}ms)`,
|
|
282
|
+
)
|
|
283
|
+
return response
|
|
284
|
+
})
|
|
285
|
+
.catch((error) => {
|
|
286
|
+
const duration = Date.now() - startTime
|
|
287
|
+
console.log(
|
|
288
|
+
`✗ ${method} ${pathname} ERROR (${duration}ms): ${error.message}`,
|
|
289
|
+
)
|
|
290
|
+
throw error
|
|
291
|
+
})
|
|
292
|
+
} else {
|
|
293
|
+
const duration = Date.now() - startTime
|
|
294
|
+
console.log(`← ${method} ${pathname} ${result.status} (${duration}ms)`)
|
|
295
|
+
return result
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const duration = Date.now() - startTime
|
|
299
|
+
console.log(
|
|
300
|
+
`✗ ${method} ${pathname} ERROR (${duration}ms): ${error.message}`,
|
|
301
|
+
)
|
|
302
|
+
throw error
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
createLogger,
|
|
309
|
+
simpleLogger,
|
|
310
|
+
}
|