0http-bun 1.2.2 → 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 +502 -9
- package/common.d.ts +16 -0
- package/lib/middleware/README.md +124 -23
- package/lib/middleware/body-parser.js +410 -263
- package/lib/middleware/cors.js +57 -57
- package/lib/middleware/index.d.ts +27 -24
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/jwt-auth.js +129 -37
- package/lib/middleware/rate-limit.js +77 -27
- package/lib/next.js +8 -1
- package/lib/router/sequential.js +41 -10
- package/package.json +7 -7
package/lib/middleware/cors.js
CHANGED
|
@@ -28,8 +28,8 @@ function createCORS(options = {}) {
|
|
|
28
28
|
const allowedOrigin = getAllowedOrigin(origin, requestOrigin, req)
|
|
29
29
|
|
|
30
30
|
const addCorsHeaders = (response) => {
|
|
31
|
-
// Add Vary header for
|
|
32
|
-
if (
|
|
31
|
+
// Add Vary header for non-wildcard origins to prevent CDN cache poisoning
|
|
32
|
+
if (origin !== '*') {
|
|
33
33
|
const existingVary = response.headers.get('Vary')
|
|
34
34
|
if (existingVary) {
|
|
35
35
|
if (!existingVary.includes('Origin')) {
|
|
@@ -42,32 +42,51 @@ function createCORS(options = {}) {
|
|
|
42
42
|
|
|
43
43
|
if (allowedOrigin !== false) {
|
|
44
44
|
response.headers.set('Access-Control-Allow-Origin', allowedOrigin)
|
|
45
|
-
}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
// Don't allow wildcard origin with credentials
|
|
47
|
+
if (credentials && allowedOrigin !== '*') {
|
|
48
|
+
response.headers.set('Access-Control-Allow-Credentials', 'true')
|
|
49
|
+
}
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
// Handle exposedHeaders (can be string or array)
|
|
52
|
+
const exposedHeadersList = Array.isArray(exposedHeaders)
|
|
53
|
+
? exposedHeaders
|
|
54
|
+
: typeof exposedHeaders === 'string'
|
|
55
|
+
? [exposedHeaders]
|
|
56
|
+
: []
|
|
57
|
+
if (exposedHeadersList.length > 0) {
|
|
58
|
+
response.headers.set(
|
|
59
|
+
'Access-Control-Expose-Headers',
|
|
60
|
+
exposedHeadersList.join(', '),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add method and header info
|
|
65
|
+
response.headers.set(
|
|
66
|
+
'Access-Control-Allow-Methods',
|
|
67
|
+
(Array.isArray(methods) ? methods : [methods]).join(', '),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const resolvedAllowedHeaders =
|
|
71
|
+
typeof allowedHeaders === 'function'
|
|
72
|
+
? allowedHeaders(req)
|
|
73
|
+
: allowedHeaders
|
|
74
|
+
const allowedHeadersList = Array.isArray(resolvedAllowedHeaders)
|
|
75
|
+
? resolvedAllowedHeaders
|
|
76
|
+
: typeof resolvedAllowedHeaders === 'string'
|
|
77
|
+
? [resolvedAllowedHeaders]
|
|
78
|
+
: []
|
|
59
79
|
response.headers.set(
|
|
60
|
-
'Access-Control-
|
|
61
|
-
|
|
80
|
+
'Access-Control-Allow-Headers',
|
|
81
|
+
allowedHeadersList.join(', '),
|
|
62
82
|
)
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'Access-Control-Allow-Methods',
|
|
68
|
-
(Array.isArray(methods) ? methods : [methods]).join(', '),
|
|
69
|
-
)
|
|
85
|
+
return response
|
|
86
|
+
}
|
|
70
87
|
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
// I-2: Resolve allowedHeaders once for the entire preflight handling
|
|
71
90
|
const resolvedAllowedHeaders =
|
|
72
91
|
typeof allowedHeaders === 'function'
|
|
73
92
|
? allowedHeaders(req)
|
|
@@ -77,15 +96,7 @@ function createCORS(options = {}) {
|
|
|
77
96
|
: typeof resolvedAllowedHeaders === 'string'
|
|
78
97
|
? [resolvedAllowedHeaders]
|
|
79
98
|
: []
|
|
80
|
-
response.headers.set(
|
|
81
|
-
'Access-Control-Allow-Headers',
|
|
82
|
-
allowedHeadersList.join(', '),
|
|
83
|
-
)
|
|
84
99
|
|
|
85
|
-
return response
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (req.method === 'OPTIONS') {
|
|
89
100
|
// Handle preflight request
|
|
90
101
|
const requestMethod = req.headers.get('access-control-request-method')
|
|
91
102
|
const requestHeaders = req.headers.get('access-control-request-headers')
|
|
@@ -98,13 +109,6 @@ function createCORS(options = {}) {
|
|
|
98
109
|
// Check if requested headers are allowed
|
|
99
110
|
if (requestHeaders) {
|
|
100
111
|
const requestedHeaders = requestHeaders.split(',').map((h) => h.trim())
|
|
101
|
-
const resolvedAllowedHeaders =
|
|
102
|
-
typeof allowedHeaders === 'function'
|
|
103
|
-
? allowedHeaders(req)
|
|
104
|
-
: allowedHeaders
|
|
105
|
-
const allowedHeadersList = Array.isArray(resolvedAllowedHeaders)
|
|
106
|
-
? resolvedAllowedHeaders
|
|
107
|
-
: []
|
|
108
112
|
|
|
109
113
|
const hasDisallowedHeaders = requestedHeaders.some(
|
|
110
114
|
(header) =>
|
|
@@ -127,31 +131,24 @@ function createCORS(options = {}) {
|
|
|
127
131
|
if (typeof origin === 'function' || Array.isArray(origin)) {
|
|
128
132
|
response.headers.set('Vary', 'Origin')
|
|
129
133
|
}
|
|
130
|
-
}
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
// Don't allow wildcard origin with credentials
|
|
136
|
+
if (credentials && allowedOrigin !== '*') {
|
|
137
|
+
response.headers.set('Access-Control-Allow-Credentials', 'true')
|
|
138
|
+
}
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
response.headers.set(
|
|
141
|
+
'Access-Control-Allow-Methods',
|
|
142
|
+
(Array.isArray(methods) ? methods : [methods]).join(', '),
|
|
143
|
+
)
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const allowedHeadersList = Array.isArray(resolvedAllowedHeaders)
|
|
147
|
-
? resolvedAllowedHeaders
|
|
148
|
-
: []
|
|
149
|
-
response.headers.set(
|
|
150
|
-
'Access-Control-Allow-Headers',
|
|
151
|
-
allowedHeadersList.join(', '),
|
|
152
|
-
)
|
|
145
|
+
response.headers.set(
|
|
146
|
+
'Access-Control-Allow-Headers',
|
|
147
|
+
allowedHeadersList.join(', '),
|
|
148
|
+
)
|
|
153
149
|
|
|
154
|
-
|
|
150
|
+
response.headers.set('Access-Control-Max-Age', maxAge.toString())
|
|
151
|
+
}
|
|
155
152
|
|
|
156
153
|
if (preflightContinue) {
|
|
157
154
|
const result = next()
|
|
@@ -193,10 +190,13 @@ function getAllowedOrigin(origin, requestOrigin, req) {
|
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
if (Array.isArray(origin)) {
|
|
193
|
+
if (!requestOrigin || requestOrigin === 'null') return false
|
|
196
194
|
return origin.includes(requestOrigin) ? requestOrigin : false
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
if (typeof origin === 'function') {
|
|
198
|
+
// Reject null/missing origins to prevent bypass via sandboxed iframes
|
|
199
|
+
if (!requestOrigin || requestOrigin === 'null') return false
|
|
200
200
|
const result = origin(requestOrigin)
|
|
201
201
|
return result === true ? requestOrigin : result || false
|
|
202
202
|
}
|
|
@@ -65,6 +65,8 @@ export interface JWTAuthOptions {
|
|
|
65
65
|
audience?: string | string[]
|
|
66
66
|
issuer?: string
|
|
67
67
|
algorithms?: string[]
|
|
68
|
+
// Token type validation
|
|
69
|
+
requiredTokenType?: string
|
|
68
70
|
// Custom response and error handling
|
|
69
71
|
unauthorizedResponse?:
|
|
70
72
|
| Response
|
|
@@ -95,29 +97,14 @@ export interface TokenExtractionOptions {
|
|
|
95
97
|
export function createJWTAuth(options?: JWTAuthOptions): RequestHandler
|
|
96
98
|
export function createAPIKeyAuth(options: APIKeyAuthOptions): RequestHandler
|
|
97
99
|
export function extractTokenFromHeader(req: ZeroRequest): string | null
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
options?: TokenExtractionOptions,
|
|
101
|
-
): string | null
|
|
102
|
-
export function validateApiKeyInternal(
|
|
103
|
-
apiKey: string,
|
|
104
|
-
apiKeys: JWTAuthOptions['apiKeys'],
|
|
105
|
-
apiKeyValidator: JWTAuthOptions['apiKeyValidator'],
|
|
106
|
-
req: ZeroRequest,
|
|
107
|
-
): Promise<boolean | any>
|
|
108
|
-
export function handleAuthError(
|
|
109
|
-
error: Error,
|
|
110
|
-
handlers: {
|
|
111
|
-
unauthorizedResponse?: JWTAuthOptions['unauthorizedResponse']
|
|
112
|
-
onError?: JWTAuthOptions['onError']
|
|
113
|
-
},
|
|
114
|
-
req: ZeroRequest,
|
|
115
|
-
): Response
|
|
100
|
+
export const API_KEY_SYMBOL: symbol
|
|
101
|
+
export function maskApiKey(key: string): string
|
|
116
102
|
|
|
117
103
|
// Rate limiting middleware types
|
|
118
104
|
export interface RateLimitOptions {
|
|
119
105
|
windowMs?: number
|
|
120
106
|
max?: number
|
|
107
|
+
message?: string
|
|
121
108
|
keyGenerator?: (req: ZeroRequest) => Promise<string> | string
|
|
122
109
|
handler?: (
|
|
123
110
|
req: ZeroRequest,
|
|
@@ -126,7 +113,7 @@ export interface RateLimitOptions {
|
|
|
126
113
|
resetTime: Date,
|
|
127
114
|
) => Promise<Response> | Response
|
|
128
115
|
store?: RateLimitStore
|
|
129
|
-
standardHeaders?: boolean
|
|
116
|
+
standardHeaders?: boolean | 'minimal'
|
|
130
117
|
excludePaths?: string[]
|
|
131
118
|
skip?: (req: ZeroRequest) => boolean
|
|
132
119
|
}
|
|
@@ -169,7 +156,7 @@ export interface CORSOptions {
|
|
|
169
156
|
| boolean
|
|
170
157
|
| ((origin: string, req: ZeroRequest) => boolean | string)
|
|
171
158
|
methods?: string[]
|
|
172
|
-
allowedHeaders?: string[]
|
|
159
|
+
allowedHeaders?: string[] | ((req: ZeroRequest) => string[])
|
|
173
160
|
exposedHeaders?: string[]
|
|
174
161
|
credentials?: boolean
|
|
175
162
|
maxAge?: number
|
|
@@ -187,25 +174,30 @@ export function getAllowedOrigin(
|
|
|
187
174
|
|
|
188
175
|
// Body parser middleware types
|
|
189
176
|
export interface JSONParserOptions {
|
|
190
|
-
limit?: number
|
|
177
|
+
limit?: number | string
|
|
191
178
|
reviver?: (key: string, value: any) => any
|
|
192
179
|
strict?: boolean
|
|
193
180
|
type?: string
|
|
181
|
+
deferNext?: boolean
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
export interface TextParserOptions {
|
|
197
|
-
limit?: number
|
|
185
|
+
limit?: number | string
|
|
198
186
|
type?: string
|
|
199
187
|
defaultCharset?: string
|
|
188
|
+
deferNext?: boolean
|
|
200
189
|
}
|
|
201
190
|
|
|
202
191
|
export interface URLEncodedParserOptions {
|
|
203
|
-
limit?: number
|
|
192
|
+
limit?: number | string
|
|
204
193
|
extended?: boolean
|
|
194
|
+
parseNestedObjects?: boolean
|
|
195
|
+
deferNext?: boolean
|
|
205
196
|
}
|
|
206
197
|
|
|
207
198
|
export interface MultipartParserOptions {
|
|
208
|
-
limit?: number
|
|
199
|
+
limit?: number | string
|
|
200
|
+
deferNext?: boolean
|
|
209
201
|
}
|
|
210
202
|
|
|
211
203
|
export interface BodyParserOptions {
|
|
@@ -213,6 +205,15 @@ export interface BodyParserOptions {
|
|
|
213
205
|
text?: TextParserOptions
|
|
214
206
|
urlencoded?: URLEncodedParserOptions
|
|
215
207
|
multipart?: MultipartParserOptions
|
|
208
|
+
jsonTypes?: string[]
|
|
209
|
+
jsonParser?: (text: string) => any
|
|
210
|
+
onError?: (error: Error, req: ZeroRequest, next: () => any) => any
|
|
211
|
+
verify?: (req: ZeroRequest, rawBody: string) => void
|
|
212
|
+
parseNestedObjects?: boolean
|
|
213
|
+
jsonLimit?: number | string
|
|
214
|
+
textLimit?: number | string
|
|
215
|
+
urlencodedLimit?: number | string
|
|
216
|
+
multipartLimit?: number | string
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
export interface ParsedFile {
|
|
@@ -233,6 +234,8 @@ export function createMultipartParser(
|
|
|
233
234
|
export function createBodyParser(options?: BodyParserOptions): RequestHandler
|
|
234
235
|
export function hasBody(req: ZeroRequest): boolean
|
|
235
236
|
export function shouldParse(req: ZeroRequest, type: string): boolean
|
|
237
|
+
export function parseLimit(limit: number | string): number
|
|
238
|
+
export const RAW_BODY_SYMBOL: symbol
|
|
236
239
|
|
|
237
240
|
// Prometheus metrics middleware types
|
|
238
241
|
export interface PrometheusMetrics {
|
package/lib/middleware/index.js
CHANGED
|
@@ -23,6 +23,8 @@ module.exports = {
|
|
|
23
23
|
createJWTAuth: jwtAuthModule.createJWTAuth,
|
|
24
24
|
createAPIKeyAuth: jwtAuthModule.createAPIKeyAuth,
|
|
25
25
|
extractTokenFromHeader: jwtAuthModule.extractTokenFromHeader,
|
|
26
|
+
API_KEY_SYMBOL: jwtAuthModule.API_KEY_SYMBOL,
|
|
27
|
+
maskApiKey: jwtAuthModule.maskApiKey,
|
|
26
28
|
|
|
27
29
|
// Rate limiting middleware
|
|
28
30
|
createRateLimit: rateLimitModule.createRateLimit,
|
|
@@ -44,6 +46,8 @@ module.exports = {
|
|
|
44
46
|
createBodyParser: bodyParserModule.createBodyParser,
|
|
45
47
|
hasBody: bodyParserModule.hasBody,
|
|
46
48
|
shouldParse: bodyParserModule.shouldParse,
|
|
49
|
+
parseLimit: bodyParserModule.parseLimit,
|
|
50
|
+
RAW_BODY_SYMBOL: bodyParserModule.RAW_BODY_SYMBOL,
|
|
47
51
|
|
|
48
52
|
// Prometheus metrics middleware
|
|
49
53
|
createPrometheusMiddleware: prometheusModule.createPrometheusMiddleware,
|
|
@@ -13,6 +13,40 @@ function loadJose() {
|
|
|
13
13
|
return joseLib
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const crypto = require('crypto')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Symbol key for storing raw API key to prevent accidental serialization (M-7 fix)
|
|
20
|
+
*/
|
|
21
|
+
const API_KEY_SYMBOL = Symbol.for('0http.apiKey')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Masks an API key for safe storage/logging.
|
|
25
|
+
* Shows first 4 and last 4 characters, masks the rest.
|
|
26
|
+
* @param {string} key - Raw API key
|
|
27
|
+
* @returns {string} Masked key
|
|
28
|
+
*/
|
|
29
|
+
function maskApiKey(key) {
|
|
30
|
+
if (!key || typeof key !== 'string') return '****'
|
|
31
|
+
if (key.length <= 8) return '****'
|
|
32
|
+
return key.slice(0, 4) + '****' + key.slice(-4)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Constant-time string comparison to prevent timing attacks
|
|
37
|
+
*/
|
|
38
|
+
function timingSafeCompare(a, b) {
|
|
39
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
40
|
+
const aBuf = Buffer.from(a)
|
|
41
|
+
const bBuf = Buffer.from(b)
|
|
42
|
+
if (aBuf.length !== bBuf.length) {
|
|
43
|
+
// Still perform comparison to maintain constant time
|
|
44
|
+
crypto.timingSafeEqual(aBuf, aBuf)
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return crypto.timingSafeEqual(aBuf, bBuf)
|
|
48
|
+
}
|
|
49
|
+
|
|
16
50
|
/**
|
|
17
51
|
* Creates JWT authentication middleware
|
|
18
52
|
* @param {Object} options - JWT configuration options
|
|
@@ -53,6 +87,7 @@ function createJWTAuth(options = {}) {
|
|
|
53
87
|
audience,
|
|
54
88
|
issuer,
|
|
55
89
|
algorithms,
|
|
90
|
+
requiredTokenType,
|
|
56
91
|
} = options
|
|
57
92
|
|
|
58
93
|
// API key mode doesn't require JWT secret
|
|
@@ -61,6 +96,35 @@ function createJWTAuth(options = {}) {
|
|
|
61
96
|
throw new Error('JWT middleware requires either secret or jwksUri')
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
// H-8: Warn about security risk of JWT tokens in query parameters
|
|
100
|
+
if (tokenQuery) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[0http-bun] SECURITY WARNING: JWT tokenQuery ("${tokenQuery}") is deprecated. ` +
|
|
103
|
+
'Tokens in query parameters are logged in server access logs, browser history, and Referer headers. ' +
|
|
104
|
+
'Use Authorization header or a custom getToken function instead.',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// M-8: Validate JWKS URI uses HTTPS in production
|
|
109
|
+
if (jwksUri) {
|
|
110
|
+
const parsedUri = new URL(jwksUri)
|
|
111
|
+
if (
|
|
112
|
+
parsedUri.protocol !== 'https:' &&
|
|
113
|
+
process.env.NODE_ENV === 'production'
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'JWT middleware: JWKS URI must use HTTPS in production to prevent MitM key substitution. ' +
|
|
117
|
+
`Got: ${parsedUri.protocol}// Set NODE_ENV to a non-production value to bypass this check.`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
if (parsedUri.protocol !== 'https:') {
|
|
121
|
+
console.warn(
|
|
122
|
+
`[0http-bun] SECURITY WARNING: JWKS URI "${jwksUri}" uses ${parsedUri.protocol} instead of https:. ` +
|
|
123
|
+
'This is insecure and will be rejected in production (NODE_ENV=production).',
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
64
128
|
// Setup key resolver for JWT
|
|
65
129
|
let keyLike
|
|
66
130
|
if (jwks) {
|
|
@@ -82,18 +146,36 @@ function createJWTAuth(options = {}) {
|
|
|
82
146
|
}
|
|
83
147
|
|
|
84
148
|
// Default JWT verification options
|
|
149
|
+
const resolvedAlgorithms = algorithms || jwtOptions.algorithms || ['HS256']
|
|
150
|
+
|
|
151
|
+
// Prevent algorithm confusion attacks
|
|
152
|
+
const hasSymmetric = resolvedAlgorithms.some((alg) => alg.startsWith('HS'))
|
|
153
|
+
const hasAsymmetric = resolvedAlgorithms.some(
|
|
154
|
+
(alg) =>
|
|
155
|
+
alg.startsWith('RS') || alg.startsWith('ES') || alg.startsWith('PS'),
|
|
156
|
+
)
|
|
157
|
+
if (hasSymmetric && hasAsymmetric) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'JWT middleware: mixing symmetric (HS*) and asymmetric (RS*/ES*/PS*) algorithms is not allowed. This prevents algorithm confusion attacks.',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
85
163
|
const defaultJwtOptions = {
|
|
86
|
-
|
|
164
|
+
...jwtOptions,
|
|
165
|
+
algorithms: resolvedAlgorithms,
|
|
87
166
|
audience,
|
|
88
167
|
issuer,
|
|
89
|
-
...jwtOptions,
|
|
90
168
|
}
|
|
91
169
|
|
|
92
170
|
return async function jwtAuthMiddleware(req, next) {
|
|
93
171
|
const url = new URL(req.url)
|
|
94
172
|
|
|
95
173
|
// Skip authentication for excluded paths
|
|
96
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
excludePaths.some(
|
|
176
|
+
(path) => url.pathname === path || url.pathname.startsWith(path + '/'),
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
97
179
|
return next()
|
|
98
180
|
}
|
|
99
181
|
|
|
@@ -109,18 +191,19 @@ function createJWTAuth(options = {}) {
|
|
|
109
191
|
req,
|
|
110
192
|
)
|
|
111
193
|
if (validationResult !== false) {
|
|
112
|
-
// Set API key context
|
|
194
|
+
// Set API key context — store masked version to prevent leakage (M-7 fix)
|
|
113
195
|
req.ctx = req.ctx || {}
|
|
114
|
-
req.ctx.apiKey = apiKey
|
|
196
|
+
req.ctx.apiKey = maskApiKey(apiKey)
|
|
197
|
+
req[API_KEY_SYMBOL] = apiKey // Raw key via Symbol for programmatic access only
|
|
115
198
|
|
|
116
199
|
// If validation result is an object, use it as user data, otherwise default
|
|
117
200
|
const userData =
|
|
118
201
|
validationResult && typeof validationResult === 'object'
|
|
119
202
|
? validationResult
|
|
120
|
-
: {apiKey}
|
|
203
|
+
: {apiKey: maskApiKey(apiKey)}
|
|
121
204
|
|
|
122
205
|
req.ctx.user = userData
|
|
123
|
-
req.apiKey = apiKey
|
|
206
|
+
req.apiKey = maskApiKey(apiKey)
|
|
124
207
|
req.user = userData
|
|
125
208
|
return next()
|
|
126
209
|
} else {
|
|
@@ -164,24 +247,40 @@ function createJWTAuth(options = {}) {
|
|
|
164
247
|
defaultJwtOptions,
|
|
165
248
|
)
|
|
166
249
|
|
|
250
|
+
// L-4: Validate JWT token type header if configured
|
|
251
|
+
if (requiredTokenType) {
|
|
252
|
+
const tokenType = protectedHeader.typ
|
|
253
|
+
if (
|
|
254
|
+
!tokenType ||
|
|
255
|
+
tokenType.toLowerCase() !== requiredTokenType.toLowerCase()
|
|
256
|
+
) {
|
|
257
|
+
return handleAuthError(
|
|
258
|
+
new Error('Invalid token type'),
|
|
259
|
+
{unauthorizedResponse, onError},
|
|
260
|
+
req,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
167
265
|
// Add user info to request context
|
|
168
266
|
req.ctx = req.ctx || {}
|
|
169
267
|
req.ctx.user = payload
|
|
170
268
|
req.ctx.jwt = {
|
|
171
269
|
payload,
|
|
172
270
|
header: protectedHeader,
|
|
173
|
-
token,
|
|
174
271
|
}
|
|
175
272
|
req.user = payload // Mirror to root for compatibility
|
|
176
273
|
req.jwt = {
|
|
177
274
|
payload,
|
|
178
275
|
header: protectedHeader,
|
|
179
|
-
token,
|
|
180
276
|
}
|
|
181
277
|
|
|
182
278
|
return next()
|
|
183
279
|
} catch (error) {
|
|
184
280
|
if (optional && (!hasApiKeyMode || !req.headers.get(apiKeyHeader))) {
|
|
281
|
+
req.ctx = req.ctx || {}
|
|
282
|
+
req.ctx.authError = error.message
|
|
283
|
+
req.ctx.authAttempted = true
|
|
185
284
|
return next()
|
|
186
285
|
}
|
|
187
286
|
|
|
@@ -200,12 +299,6 @@ function createJWTAuth(options = {}) {
|
|
|
200
299
|
*/
|
|
201
300
|
async function validateApiKeyInternal(apiKey, apiKeys, apiKeyValidator, req) {
|
|
202
301
|
if (apiKeyValidator) {
|
|
203
|
-
// Check if this is the simplified validateApiKey function (expects only key)
|
|
204
|
-
if (apiKeyValidator.length === 1) {
|
|
205
|
-
const result = await apiKeyValidator(apiKey)
|
|
206
|
-
return result || false
|
|
207
|
-
}
|
|
208
|
-
// Otherwise call with both key and req
|
|
209
302
|
const result = await apiKeyValidator(apiKey, req)
|
|
210
303
|
return result || false
|
|
211
304
|
}
|
|
@@ -216,10 +309,10 @@ async function validateApiKeyInternal(apiKey, apiKeys, apiKeyValidator, req) {
|
|
|
216
309
|
}
|
|
217
310
|
|
|
218
311
|
if (Array.isArray(apiKeys)) {
|
|
219
|
-
return apiKeys.
|
|
312
|
+
return apiKeys.some((key) => timingSafeCompare(key, apiKey))
|
|
220
313
|
}
|
|
221
314
|
|
|
222
|
-
return apiKeys
|
|
315
|
+
return timingSafeCompare(apiKeys, apiKey)
|
|
223
316
|
}
|
|
224
317
|
|
|
225
318
|
/**
|
|
@@ -271,7 +364,8 @@ function handleAuthError(error, handlers = {}, req) {
|
|
|
271
364
|
return result
|
|
272
365
|
}
|
|
273
366
|
} catch (handlerError) {
|
|
274
|
-
//
|
|
367
|
+
// I-4: Log errors from custom handlers to aid debugging
|
|
368
|
+
console.error('[0http-bun] Custom onError handler threw:', handlerError)
|
|
275
369
|
}
|
|
276
370
|
}
|
|
277
371
|
|
|
@@ -303,7 +397,11 @@ function handleAuthError(error, handlers = {}, req) {
|
|
|
303
397
|
}
|
|
304
398
|
}
|
|
305
399
|
} catch (responseError) {
|
|
306
|
-
//
|
|
400
|
+
// I-4: Log errors from custom response handlers to aid debugging
|
|
401
|
+
console.error(
|
|
402
|
+
'[0http-bun] Custom unauthorizedResponse handler threw:',
|
|
403
|
+
responseError,
|
|
404
|
+
)
|
|
307
405
|
}
|
|
308
406
|
}
|
|
309
407
|
|
|
@@ -317,19 +415,10 @@ function handleAuthError(error, handlers = {}, req) {
|
|
|
317
415
|
message = 'Invalid API key'
|
|
318
416
|
} else if (error.message === 'JWT verification not configured') {
|
|
319
417
|
message = 'JWT verification not configured'
|
|
418
|
+
} else if (error.message === 'Invalid token type') {
|
|
419
|
+
message = 'Invalid token type'
|
|
320
420
|
} else {
|
|
321
|
-
|
|
322
|
-
if (error instanceof errors.JWTExpired) {
|
|
323
|
-
message = 'Token expired'
|
|
324
|
-
} else if (error instanceof errors.JWTInvalid) {
|
|
325
|
-
message = 'Invalid token format'
|
|
326
|
-
} else if (error instanceof errors.JWKSNoMatchingKey) {
|
|
327
|
-
message = 'Token signature verification failed'
|
|
328
|
-
} else if (error.message.includes('audience')) {
|
|
329
|
-
message = 'Invalid token audience'
|
|
330
|
-
} else if (error.message.includes('issuer')) {
|
|
331
|
-
message = 'Invalid token issuer'
|
|
332
|
-
}
|
|
421
|
+
message = 'Invalid or expired token'
|
|
333
422
|
}
|
|
334
423
|
|
|
335
424
|
return new Response(JSON.stringify({error: message}), {
|
|
@@ -376,7 +465,10 @@ function createAPIKeyAuth(options = {}) {
|
|
|
376
465
|
const validateKey =
|
|
377
466
|
typeof keys === 'function'
|
|
378
467
|
? keys
|
|
379
|
-
: (key) =>
|
|
468
|
+
: (key) =>
|
|
469
|
+
Array.isArray(keys)
|
|
470
|
+
? keys.some((k) => timingSafeCompare(k, key))
|
|
471
|
+
: timingSafeCompare(keys, key)
|
|
380
472
|
|
|
381
473
|
return async function apiKeyAuthMiddleware(req, next) {
|
|
382
474
|
try {
|
|
@@ -400,9 +492,10 @@ function createAPIKeyAuth(options = {}) {
|
|
|
400
492
|
})
|
|
401
493
|
}
|
|
402
494
|
|
|
403
|
-
// Add API key info to context
|
|
495
|
+
// Add API key info to context — store masked version (M-7 fix)
|
|
404
496
|
req.ctx = req.ctx || {}
|
|
405
|
-
req.ctx.apiKey = apiKey
|
|
497
|
+
req.ctx.apiKey = maskApiKey(apiKey)
|
|
498
|
+
req[API_KEY_SYMBOL] = apiKey // Raw key via Symbol for programmatic access only
|
|
406
499
|
|
|
407
500
|
return next()
|
|
408
501
|
} catch (error) {
|
|
@@ -418,7 +511,6 @@ module.exports = {
|
|
|
418
511
|
createJWTAuth,
|
|
419
512
|
createAPIKeyAuth,
|
|
420
513
|
extractTokenFromHeader,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
handleAuthError,
|
|
514
|
+
API_KEY_SYMBOL,
|
|
515
|
+
maskApiKey,
|
|
424
516
|
}
|