@stacksjs/bun-router 0.0.14 → 0.0.15
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 +261 -584
- package/bin/cli.ts +9 -0
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/chunk-j0e7z7hd.js +18053 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3360 -0
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13968 -0
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +256 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +114 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +317 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1794 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +45 -44
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +682 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +220 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +275 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +665 -0
- package/src/response/response-factory.ts +596 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +690 -0
- package/src/router/file-streaming.ts +383 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +187 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +221 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1604 -0
- package/src/router/server.ts +462 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2067 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
- package/CHANGELOG.md +0 -770
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { EnhancedRequest, Middleware, NextFunction } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface AuthOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Authentication type to use
|
|
6
|
+
*/
|
|
7
|
+
type: 'basic' | 'bearer' | 'api-key' | 'oauth2' | 'jwt' | 'custom'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Function to validate credentials
|
|
11
|
+
*/
|
|
12
|
+
validator: (credentials: any, req: EnhancedRequest) => Promise<boolean> | boolean
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract user information after successful authentication
|
|
16
|
+
*/
|
|
17
|
+
userExtractor?: (credentials: any, req: EnhancedRequest) => Promise<any> | any
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where to look for auth information
|
|
21
|
+
*/
|
|
22
|
+
source?: 'header' | 'query' | 'cookie'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Key name for the source (header name, query param, cookie name)
|
|
26
|
+
*/
|
|
27
|
+
key?: string
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Realm for Basic authentication
|
|
31
|
+
*/
|
|
32
|
+
realm?: string
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom credentials extractor
|
|
36
|
+
*/
|
|
37
|
+
credentialsExtractor?: (req: EnhancedRequest) => Promise<any> | any
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether to attach user info to request
|
|
41
|
+
*/
|
|
42
|
+
attachUser?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extracts and decodes Basic Auth credentials from Authorization header
|
|
47
|
+
*/
|
|
48
|
+
export function extractBasicAuth(authHeader: string): { username: string, password: string } | null {
|
|
49
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const base64Credentials = authHeader.split(' ')[1]
|
|
55
|
+
const credentials = atob(base64Credentials)
|
|
56
|
+
const [username, password] = credentials.split(':')
|
|
57
|
+
|
|
58
|
+
return { username, password }
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
void error
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extracts Bearer token from Authorization header
|
|
69
|
+
*/
|
|
70
|
+
export function extractBearerToken(authHeader: string): string | null {
|
|
71
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return authHeader.split(' ')[1]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extracts API key from various sources
|
|
80
|
+
*/
|
|
81
|
+
export function extractApiKey(req: EnhancedRequest, source: 'header' | 'query' | 'cookie' = 'header', key: string = 'X-API-Key'): string | null {
|
|
82
|
+
switch (source) {
|
|
83
|
+
case 'header':
|
|
84
|
+
return req.headers.get(key) || null
|
|
85
|
+
case 'query':
|
|
86
|
+
return new URL(req.url).searchParams.get(key)
|
|
87
|
+
case 'cookie':
|
|
88
|
+
// Handle both Map-like cookies (with .get()) and plain object cookies
|
|
89
|
+
if (req.cookies) {
|
|
90
|
+
if (typeof req.cookies.get === 'function') {
|
|
91
|
+
return req.cookies.get(key) || null
|
|
92
|
+
}
|
|
93
|
+
return (req.cookies as unknown as Record<string, string>)[key] || null
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
default:
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default class AuthMiddleware implements Middleware {
|
|
102
|
+
private options: AuthOptions
|
|
103
|
+
|
|
104
|
+
constructor(options: AuthOptions) {
|
|
105
|
+
this.options = {
|
|
106
|
+
source: 'header',
|
|
107
|
+
key: 'Authorization',
|
|
108
|
+
attachUser: true,
|
|
109
|
+
...options,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
114
|
+
let credentials: any = null
|
|
115
|
+
|
|
116
|
+
// Extract credentials based on auth type
|
|
117
|
+
if (this.options.credentialsExtractor) {
|
|
118
|
+
credentials = await this.options.credentialsExtractor(req)
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
switch (this.options.type) {
|
|
122
|
+
case 'basic': {
|
|
123
|
+
const authHeader = req.headers.get(this.options.key || 'Authorization')
|
|
124
|
+
credentials = extractBasicAuth(authHeader || '')
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
case 'bearer':
|
|
128
|
+
case 'jwt': {
|
|
129
|
+
const authHeader = req.headers.get(this.options.key || 'Authorization')
|
|
130
|
+
credentials = extractBearerToken(authHeader || '')
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
case 'api-key': {
|
|
134
|
+
credentials = extractApiKey(req, this.options.source, this.options.key)
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
case 'oauth2': {
|
|
138
|
+
// OAuth tokens are typically sent as Bearer tokens
|
|
139
|
+
const authHeader = req.headers.get(this.options.key || 'Authorization')
|
|
140
|
+
credentials = extractBearerToken(authHeader || '')
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If no credentials were found, return 401
|
|
147
|
+
if (!credentials) {
|
|
148
|
+
const headers: Record<string, string> = {}
|
|
149
|
+
|
|
150
|
+
// For Basic auth, include WWW-Authenticate header
|
|
151
|
+
if (this.options.type === 'basic') {
|
|
152
|
+
const realm = this.options.realm || 'Secure Area'
|
|
153
|
+
headers['WWW-Authenticate'] = `Basic realm="${realm}"`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return new Response('Unauthorized', {
|
|
157
|
+
status: 401,
|
|
158
|
+
headers,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate credentials
|
|
163
|
+
const isValid = await this.options.validator(credentials, req)
|
|
164
|
+
|
|
165
|
+
if (!isValid) {
|
|
166
|
+
return new Response('Unauthorized', { status: 401 })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If user extractor provided and attachUser is true, attach user to request
|
|
170
|
+
if (this.options.userExtractor && this.options.attachUser) {
|
|
171
|
+
const user = await this.options.userExtractor(credentials, req)
|
|
172
|
+
;(req as any).user = user
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Authentication successful, continue to next middleware or route handler
|
|
176
|
+
const response = await next()
|
|
177
|
+
return response || new Response('Not Found', { status: 404 })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Convenience factory functions for common auth types
|
|
182
|
+
export function basicAuth(validator: (credentials: { username: string, password: string }, req: EnhancedRequest) => Promise<boolean> | boolean, options: Partial<AuthOptions> = {}): AuthMiddleware {
|
|
183
|
+
return new AuthMiddleware({
|
|
184
|
+
type: 'basic',
|
|
185
|
+
validator,
|
|
186
|
+
...options,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function bearerAuth(validator: (token: string, req: EnhancedRequest) => Promise<boolean> | boolean, options: Partial<AuthOptions> = {}): AuthMiddleware {
|
|
191
|
+
return new AuthMiddleware({
|
|
192
|
+
type: 'bearer',
|
|
193
|
+
validator,
|
|
194
|
+
...options,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function jwtAuth(validator: (token: string, req: EnhancedRequest) => Promise<boolean> | boolean, options: Partial<AuthOptions> = {}): AuthMiddleware {
|
|
199
|
+
return new AuthMiddleware({
|
|
200
|
+
type: 'jwt',
|
|
201
|
+
validator,
|
|
202
|
+
...options,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function apiKeyAuth(validator: (apiKey: string, req: EnhancedRequest) => Promise<boolean> | boolean, options: Partial<AuthOptions> = {}): AuthMiddleware {
|
|
207
|
+
return new AuthMiddleware({
|
|
208
|
+
type: 'api-key',
|
|
209
|
+
validator,
|
|
210
|
+
...options,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function oauth2Auth(validator: (token: string, req: EnhancedRequest) => Promise<boolean> | boolean, options: Partial<AuthOptions> = {}): AuthMiddleware {
|
|
215
|
+
return new AuthMiddleware({
|
|
216
|
+
type: 'oauth2',
|
|
217
|
+
validator,
|
|
218
|
+
...options,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { EnhancedRequest, NextFunction } from '../types'
|
|
2
|
+
import { config } from '../config'
|
|
3
|
+
|
|
4
|
+
export interface CSPDirectives {
|
|
5
|
+
'default-src'?: string[]
|
|
6
|
+
'script-src'?: string[]
|
|
7
|
+
'script-src-elem'?: string[]
|
|
8
|
+
'script-src-attr'?: string[]
|
|
9
|
+
'style-src'?: string[]
|
|
10
|
+
'style-src-elem'?: string[]
|
|
11
|
+
'style-src-attr'?: string[]
|
|
12
|
+
'img-src'?: string[]
|
|
13
|
+
'font-src'?: string[]
|
|
14
|
+
'connect-src'?: string[]
|
|
15
|
+
'media-src'?: string[]
|
|
16
|
+
'object-src'?: string[]
|
|
17
|
+
'child-src'?: string[]
|
|
18
|
+
'frame-src'?: string[]
|
|
19
|
+
'worker-src'?: string[]
|
|
20
|
+
'frame-ancestors'?: string[]
|
|
21
|
+
'form-action'?: string[]
|
|
22
|
+
'base-uri'?: string[]
|
|
23
|
+
'manifest-src'?: string[]
|
|
24
|
+
'prefetch-src'?: string[]
|
|
25
|
+
'navigate-to'?: string[]
|
|
26
|
+
'report-uri'?: string[]
|
|
27
|
+
'report-to'?: string[]
|
|
28
|
+
'require-trusted-types-for'?: string[]
|
|
29
|
+
'trusted-types'?: string[]
|
|
30
|
+
'upgrade-insecure-requests'?: boolean
|
|
31
|
+
'block-all-mixed-content'?: boolean
|
|
32
|
+
'plugin-types'?: string[]
|
|
33
|
+
'sandbox'?: string[]
|
|
34
|
+
'disown-opener'?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CSPOptions {
|
|
38
|
+
directives?: CSPDirectives
|
|
39
|
+
reportOnly?: boolean
|
|
40
|
+
reportUri?: string
|
|
41
|
+
reportTo?: string
|
|
42
|
+
useNonces?: boolean
|
|
43
|
+
nonceLength?: number
|
|
44
|
+
upgradeInsecureRequests?: boolean
|
|
45
|
+
blockAllMixedContent?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default class ContentSecurityPolicy {
|
|
49
|
+
private options: CSPOptions
|
|
50
|
+
private nonces: Map<string, string> = new Map()
|
|
51
|
+
|
|
52
|
+
constructor(options: CSPOptions = {}) {
|
|
53
|
+
const cspConfig = config.server?.security?.helmet?.contentSecurityPolicy
|
|
54
|
+
const defaultDirectives: CSPDirectives = {
|
|
55
|
+
'default-src': ['\'self\''],
|
|
56
|
+
'script-src': ['\'self\''],
|
|
57
|
+
'style-src': ['\'self\''],
|
|
58
|
+
'img-src': ['\'self\'', 'data:', 'https:'],
|
|
59
|
+
'font-src': ['\'self\''],
|
|
60
|
+
'connect-src': ['\'self\''],
|
|
61
|
+
'media-src': ['\'self\''],
|
|
62
|
+
'object-src': ['\'none\''],
|
|
63
|
+
'child-src': ['\'self\''],
|
|
64
|
+
'worker-src': ['\'self\''],
|
|
65
|
+
'frame-ancestors': ['\'none\''],
|
|
66
|
+
'form-action': ['\'self\''],
|
|
67
|
+
'base-uri': ['\'self\''],
|
|
68
|
+
'manifest-src': ['\'self\''],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.options = {
|
|
72
|
+
directives: options.directives || (typeof cspConfig === 'object' && cspConfig.directives) || defaultDirectives,
|
|
73
|
+
reportOnly: options.reportOnly ?? (typeof cspConfig === 'object' && cspConfig.reportOnly) ?? false,
|
|
74
|
+
reportUri: options.reportUri,
|
|
75
|
+
reportTo: options.reportTo,
|
|
76
|
+
useNonces: options.useNonces ?? false,
|
|
77
|
+
nonceLength: options.nonceLength ?? 16,
|
|
78
|
+
upgradeInsecureRequests: options.upgradeInsecureRequests ?? true,
|
|
79
|
+
blockAllMixedContent: options.blockAllMixedContent ?? false,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private generateNonce(): string {
|
|
84
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
85
|
+
let result = ''
|
|
86
|
+
for (let i = 0; i < (this.options.nonceLength || 16); i++) {
|
|
87
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private buildCSPHeader(): string {
|
|
93
|
+
const directives: string[] = []
|
|
94
|
+
const dirs = this.options.directives || {}
|
|
95
|
+
|
|
96
|
+
// Handle regular directives
|
|
97
|
+
for (const [directive, values] of Object.entries(dirs)) {
|
|
98
|
+
if (directive === 'upgrade-insecure-requests' || directive === 'block-all-mixed-content' || directive === 'disown-opener') {
|
|
99
|
+
if (values === true) {
|
|
100
|
+
directives.push(directive.replace(/([A-Z])/g, '-$1').toLowerCase())
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(values) && values.length > 0) {
|
|
106
|
+
const directiveName = directive.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
107
|
+
directives.push(`${directiveName} ${values.join(' ')}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add upgrade-insecure-requests if enabled
|
|
112
|
+
if (this.options.upgradeInsecureRequests) {
|
|
113
|
+
directives.push('upgrade-insecure-requests')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add block-all-mixed-content if enabled
|
|
117
|
+
if (this.options.blockAllMixedContent) {
|
|
118
|
+
directives.push('block-all-mixed-content')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add report-uri if specified
|
|
122
|
+
if (this.options.reportUri) {
|
|
123
|
+
directives.push(`report-uri ${this.options.reportUri}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add report-to if specified
|
|
127
|
+
if (this.options.reportTo) {
|
|
128
|
+
directives.push(`report-to ${this.options.reportTo}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return directives.join('; ')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
135
|
+
const helmetConfig = (config.server?.security?.helmet || {}) as any
|
|
136
|
+
|
|
137
|
+
// If CSP is disabled, continue to next middleware
|
|
138
|
+
if (helmetConfig.contentSecurityPolicy === false) {
|
|
139
|
+
const response = await next()
|
|
140
|
+
return response || new Response('Not Found', { status: 404 })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generate nonce if needed
|
|
144
|
+
let nonce: string | undefined
|
|
145
|
+
if (this.options.useNonces) {
|
|
146
|
+
nonce = this.generateNonce()
|
|
147
|
+
this.nonces.set(req.url, nonce)
|
|
148
|
+
|
|
149
|
+
// Add nonce to script-src and style-src if they exist
|
|
150
|
+
if (this.options.directives?.['script-src']) {
|
|
151
|
+
this.options.directives['script-src'] = [
|
|
152
|
+
...this.options.directives['script-src'],
|
|
153
|
+
`'nonce-${nonce}'`,
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
if (this.options.directives?.['style-src']) {
|
|
157
|
+
this.options.directives['style-src'] = [
|
|
158
|
+
...this.options.directives['style-src'],
|
|
159
|
+
`'nonce-${nonce}'`,
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add nonce to request context for use in templates
|
|
165
|
+
if (nonce) {
|
|
166
|
+
req.nonce = nonce
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const response = await next()
|
|
170
|
+
if (!response) {
|
|
171
|
+
return new Response('Not Found', { status: 404 })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const headers = new Headers(response.headers)
|
|
175
|
+
const cspHeader = this.buildCSPHeader()
|
|
176
|
+
|
|
177
|
+
if (cspHeader) {
|
|
178
|
+
const headerName = this.options.reportOnly
|
|
179
|
+
? 'Content-Security-Policy-Report-Only'
|
|
180
|
+
: 'Content-Security-Policy'
|
|
181
|
+
headers.set(headerName, cspHeader)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Set up reporting endpoint if specified
|
|
185
|
+
if (this.options.reportTo) {
|
|
186
|
+
headers.set('Report-To', JSON.stringify({
|
|
187
|
+
group: this.options.reportTo,
|
|
188
|
+
max_age: 10886400,
|
|
189
|
+
endpoints: [{ url: this.options.reportUri || '/csp-report' }],
|
|
190
|
+
}))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Clean up nonce after use
|
|
194
|
+
if (nonce) {
|
|
195
|
+
this.nonces.delete(req.url)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return new Response(response.body, {
|
|
199
|
+
status: response.status,
|
|
200
|
+
statusText: response.statusText,
|
|
201
|
+
headers,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Helper method to get nonce for templates
|
|
206
|
+
getNonce(url: string): string | undefined {
|
|
207
|
+
return this.nonces.get(url)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Factory function for easy use
|
|
212
|
+
export function contentSecurityPolicy(options: CSPOptions = {}): (req: EnhancedRequest, next: NextFunction) => Promise<Response> {
|
|
213
|
+
const instance = new ContentSecurityPolicy(options)
|
|
214
|
+
return async (req: EnhancedRequest, next: NextFunction) => instance.handle(req, next)
|
|
215
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { EnhancedRequest, NextFunction } from '../types'
|
|
2
|
+
import { config } from '../config'
|
|
3
|
+
|
|
4
|
+
// Default CORS headers
|
|
5
|
+
const DEFAULT_CORS_HEADERS: Record<string, string> = {
|
|
6
|
+
'Access-Control-Allow-Origin': '*',
|
|
7
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
8
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Accept, Origin',
|
|
9
|
+
'Access-Control-Max-Age': '86400',
|
|
10
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default class Cors {
|
|
14
|
+
private getCorsHeaders(): Record<string, string> {
|
|
15
|
+
const corsConfig = (config.server as any)?.cors
|
|
16
|
+
if (corsConfig && corsConfig.enabled) {
|
|
17
|
+
return {
|
|
18
|
+
'Access-Control-Allow-Origin': corsConfig.origin || '*',
|
|
19
|
+
'Access-Control-Allow-Methods': Array.isArray(corsConfig.methods) ? corsConfig.methods.join(', ') : DEFAULT_CORS_HEADERS['Access-Control-Allow-Methods'],
|
|
20
|
+
'Access-Control-Allow-Headers': Array.isArray(corsConfig.allowedHeaders) ? corsConfig.allowedHeaders.join(', ') : DEFAULT_CORS_HEADERS['Access-Control-Allow-Headers'],
|
|
21
|
+
'Access-Control-Max-Age': String(corsConfig.maxAge || 86400),
|
|
22
|
+
'Access-Control-Allow-Credentials': String(corsConfig.credentials || false),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { ...DEFAULT_CORS_HEADERS }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
29
|
+
const corsHeaders = this.getCorsHeaders()
|
|
30
|
+
|
|
31
|
+
// Handle preflight OPTIONS request
|
|
32
|
+
if (req.method === 'OPTIONS') {
|
|
33
|
+
return new Response(null, { status: 204, headers: corsHeaders })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to add CORS headers to any response
|
|
37
|
+
const addCorsHeaders = (response: Response): Response => {
|
|
38
|
+
const newHeaders = new Headers(response.headers)
|
|
39
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
40
|
+
newHeaders.set(key, value)
|
|
41
|
+
})
|
|
42
|
+
return new Response(response.body, {
|
|
43
|
+
status: response.status,
|
|
44
|
+
statusText: response.statusText,
|
|
45
|
+
headers: newHeaders,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// For non-OPTIONS requests, add CORS headers to the response
|
|
51
|
+
const response = await next()
|
|
52
|
+
if (!response) {
|
|
53
|
+
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
54
|
+
status: 404,
|
|
55
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add CORS headers to the response
|
|
60
|
+
return addCorsHeaders(response)
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Error occurred - return error response WITH CORS headers
|
|
64
|
+
console.error('[CORS] Error in middleware chain:', error)
|
|
65
|
+
return new Response(JSON.stringify({
|
|
66
|
+
error: 'Internal Server Error',
|
|
67
|
+
message: error instanceof Error ? error.message : String(error),
|
|
68
|
+
}), {
|
|
69
|
+
status: 500,
|
|
70
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { EnhancedRequest, NextFunction } from '../types'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { config } from '../config'
|
|
4
|
+
|
|
5
|
+
export default class Csrf {
|
|
6
|
+
private static tokens = new Map<string, string>()
|
|
7
|
+
|
|
8
|
+
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
9
|
+
const csrfConfig = config.server?.security?.csrf || {} as any
|
|
10
|
+
|
|
11
|
+
// Skip CSRF protection for safe methods
|
|
12
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
13
|
+
const response = await next()
|
|
14
|
+
return response || new Response('Not Found', { status: 404 })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Skip for methods that don't modify state
|
|
18
|
+
const method = req.method.toUpperCase()
|
|
19
|
+
const ignoredMethods = ['GET', 'HEAD', 'OPTIONS', ...(csrfConfig?.ignoreMethods || [])]
|
|
20
|
+
|
|
21
|
+
if (ignoredMethods.includes(method)) {
|
|
22
|
+
// For safe methods, generate a new token for the response
|
|
23
|
+
const token = this.generateToken(csrfConfig?.secret || 'csrf-secret')
|
|
24
|
+
|
|
25
|
+
// Continue to next middleware
|
|
26
|
+
const response = await next()
|
|
27
|
+
if (!response) {
|
|
28
|
+
return new Response('Not Found', { status: 404 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add CSRF token cookie to response
|
|
32
|
+
const cookieName = csrfConfig?.cookie?.name || 'csrf-token'
|
|
33
|
+
const cookieOptions = []
|
|
34
|
+
|
|
35
|
+
cookieOptions.push(`${cookieName}=${token}`)
|
|
36
|
+
cookieOptions.push('Path=/')
|
|
37
|
+
|
|
38
|
+
if (csrfConfig?.cookie?.options?.httpOnly) {
|
|
39
|
+
cookieOptions.push('HttpOnly')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (csrfConfig?.cookie?.options?.secure) {
|
|
43
|
+
cookieOptions.push('Secure')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (csrfConfig?.cookie?.options?.sameSite) {
|
|
47
|
+
cookieOptions.push(`SameSite=${csrfConfig.cookie.options.sameSite}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const newHeaders = new Headers(response.headers)
|
|
51
|
+
newHeaders.append('Set-Cookie', cookieOptions.join('; '))
|
|
52
|
+
|
|
53
|
+
// Store token for verification
|
|
54
|
+
Csrf.tokens.set(token, token)
|
|
55
|
+
|
|
56
|
+
return new Response(response.body, {
|
|
57
|
+
status: response.status,
|
|
58
|
+
statusText: response.statusText,
|
|
59
|
+
headers: newHeaders,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For unsafe methods, verify the CSRF token
|
|
64
|
+
// Check for token in headers or in the request body
|
|
65
|
+
const token = req.headers.get('X-CSRF-TOKEN')
|
|
66
|
+
|| req.jsonBody?.csrf_token
|
|
67
|
+
|| req.jsonBody?._token
|
|
68
|
+
|| null
|
|
69
|
+
|
|
70
|
+
// Get token from cookies for comparison
|
|
71
|
+
const cookies = this.parseCookies(req)
|
|
72
|
+
const cookieToken = cookies[csrfConfig?.cookie?.name || 'csrf-token']
|
|
73
|
+
|
|
74
|
+
// Verify the token
|
|
75
|
+
if (!token || !cookieToken || token !== cookieToken || !Csrf.tokens.has(token)) {
|
|
76
|
+
return new Response(
|
|
77
|
+
JSON.stringify({ error: 'CSRF token validation failed' }),
|
|
78
|
+
{
|
|
79
|
+
status: 403,
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Continue to next middleware if token is valid
|
|
86
|
+
const response = await next()
|
|
87
|
+
return response || new Response('Not Found', { status: 404 })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private generateToken(secret: string): string {
|
|
91
|
+
const randomString = randomBytes(16).toString('hex')
|
|
92
|
+
return createHash('sha256')
|
|
93
|
+
.update(`${randomString}${secret}`)
|
|
94
|
+
.digest('hex')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private parseCookies(req: Request): Record<string, string> {
|
|
98
|
+
const cookieHeader = req.headers.get('cookie')
|
|
99
|
+
if (!cookieHeader)
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
return cookieHeader.split(';').reduce((cookies, cookie) => {
|
|
103
|
+
const [name, value] = cookie.trim().split('=')
|
|
104
|
+
cookies[name] = value
|
|
105
|
+
return cookies
|
|
106
|
+
}, {} as Record<string, string>)
|
|
107
|
+
}
|
|
108
|
+
}
|