@stacksjs/bun-router 0.0.17 → 0.0.18
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/dist/cli.js +1 -1
- package/dist/container/service-provider.d.ts +3 -3
- package/dist/development/performance-profiler.d.ts +3 -3
- package/dist/development/route-debugger.d.ts +1 -1
- package/dist/development/route-inspector.d.ts +2 -2
- package/dist/observability/integration.d.ts +1 -1
- package/dist/response/macros.d.ts +1 -1
- package/dist/router/index.d.ts +7 -7
- package/dist/router/router.d.ts +0 -32
- package/dist/testing/performance-testing.d.ts +2 -2
- package/dist/testing/test-client.d.ts +1 -1
- package/dist/testing/websocket-testing.d.ts +1 -1
- package/package.json +2 -3
- package/src/auth.ts +0 -489
- package/src/cache/lru-cache.ts +0 -457
- package/src/cache/middleware-memoization.ts +0 -531
- package/src/cache/route-cache-warmer.ts +0 -486
- package/src/cache/sqlite-cache.ts +0 -783
- package/src/cache/streaming-cache.ts +0 -572
- package/src/cli/colors.ts +0 -29
- package/src/cli/index.ts +0 -287
- package/src/cli/middleware.ts +0 -291
- package/src/cli/openapi.ts +0 -407
- package/src/cli/router.ts +0 -188
- package/src/cli/routes.ts +0 -265
- package/src/cli/utils.ts +0 -531
- package/src/cli.ts +0 -5
- package/src/config.ts +0 -322
- package/src/container/container.ts +0 -786
- package/src/container/contextual-binding.ts +0 -606
- package/src/container/decorators.ts +0 -368
- package/src/container/index.ts +0 -49
- package/src/container/service-provider.ts +0 -596
- package/src/development/hot-reload.ts +0 -673
- package/src/development/index.ts +0 -499
- package/src/development/performance-profiler.ts +0 -717
- package/src/development/route-debugger.ts +0 -527
- package/src/development/route-inspector.ts +0 -749
- package/src/development/typescript-utilities.ts +0 -682
- package/src/docs.ts +0 -397
- package/src/errors/circuit-breaker.ts +0 -733
- package/src/errors/error-handler.ts +0 -569
- package/src/errors/error-reporting.ts +0 -672
- package/src/errors/exceptions.ts +0 -536
- package/src/errors/graceful-degradation.ts +0 -623
- package/src/errors/index.ts +0 -21
- package/src/errors/router-errors.ts +0 -632
- package/src/file-serving/static-files.ts +0 -718
- package/src/index.ts +0 -66
- package/src/middleware/auth.ts +0 -220
- package/src/middleware/content_security_policy.ts +0 -215
- package/src/middleware/cors.ts +0 -142
- package/src/middleware/csrf.ts +0 -166
- package/src/middleware/ddos_protection.ts +0 -279
- package/src/middleware/file_security.ts +0 -191
- package/src/middleware/file_upload.ts +0 -275
- package/src/middleware/helmet.ts +0 -268
- package/src/middleware/index.ts +0 -117
- package/src/middleware/input_validation.ts +0 -453
- package/src/middleware/json_body.ts +0 -37
- package/src/middleware/performance_alerting.ts +0 -540
- package/src/middleware/performance_dashboard.ts +0 -661
- package/src/middleware/performance_monitor.ts +0 -945
- package/src/middleware/pipeline.ts +0 -489
- package/src/middleware/rate_limit.ts +0 -251
- package/src/middleware/request_id.ts +0 -36
- package/src/middleware/request_signing.ts +0 -638
- package/src/middleware/request_tracer.ts +0 -653
- package/src/middleware/response_cache.ts +0 -778
- package/src/middleware/security.ts +0 -482
- package/src/middleware/security_suite.ts +0 -257
- package/src/middleware/session.ts +0 -91
- package/src/model-binding/index.ts +0 -18
- package/src/model-binding/model-middleware.ts +0 -425
- package/src/model-binding/model-registry.ts +0 -550
- package/src/model-binding.ts +0 -370
- package/src/model-resolver-factory.ts +0 -106
- package/src/observability/correlation.ts +0 -691
- package/src/observability/health-checks.ts +0 -729
- package/src/observability/index.ts +0 -184
- package/src/observability/integration.ts +0 -548
- package/src/observability/metrics.ts +0 -753
- package/src/observability/tracing.ts +0 -638
- package/src/optimization/bun-utilities.ts +0 -778
- package/src/query-builder-integration.ts +0 -137
- package/src/request/context.ts +0 -109
- package/src/request/enhanced-request.ts +0 -876
- package/src/request/macros.ts +0 -822
- package/src/response/macros.ts +0 -665
- package/src/response/response-factory.ts +0 -596
- package/src/router/api-routes.ts +0 -243
- package/src/router/file-based-routing.ts +0 -697
- package/src/router/file-streaming.ts +0 -383
- package/src/router/fluent-routing.ts +0 -866
- package/src/router/group-organization.ts +0 -229
- package/src/router/handler-resolver.ts +0 -327
- package/src/router/http-methods.ts +0 -407
- package/src/router/index.ts +0 -193
- package/src/router/middleware-groups.ts +0 -222
- package/src/router/middleware-integration.ts +0 -399
- package/src/router/middleware.ts +0 -234
- package/src/router/model-binding.ts +0 -215
- package/src/router/optimized-route-matching.ts +0 -302
- package/src/router/route-building.ts +0 -221
- package/src/router/route-compiler.ts +0 -691
- package/src/router/route-matching.ts +0 -368
- package/src/router/route-trie.ts +0 -450
- package/src/router/router.ts +0 -1616
- package/src/router/server.ts +0 -684
- package/src/router/validation-integration.ts +0 -449
- package/src/router/view-rendering.ts +0 -233
- package/src/router/websocket.ts +0 -100
- package/src/routing/route-caching.ts +0 -402
- package/src/routing/route-throttling.ts +0 -469
- package/src/routing/subdomain-routing.ts +0 -492
- package/src/session/database-store.ts +0 -109
- package/src/session/file-store.ts +0 -148
- package/src/session/index.ts +0 -244
- package/src/session/memory-store.ts +0 -88
- package/src/session/redis-store.ts +0 -93
- package/src/streaming/index.ts +0 -17
- package/src/streaming/sse-handler.ts +0 -482
- package/src/streaming/stream-handler.ts +0 -552
- package/src/testing/auth-testing.ts +0 -448
- package/src/testing/file-upload-testing.ts +0 -543
- package/src/testing/index.ts +0 -10
- package/src/testing/middleware-testing.ts +0 -322
- package/src/testing/model-binding-testing.ts +0 -645
- package/src/testing/performance-testing.ts +0 -738
- package/src/testing/test-client.ts +0 -310
- package/src/testing/test-request.ts +0 -315
- package/src/testing/test-response.ts +0 -332
- package/src/testing/types.ts +0 -224
- package/src/testing/websocket-testing.ts +0 -590
- package/src/types/controller-types.ts +0 -385
- package/src/types/core.ts +0 -683
- package/src/types/middleware-types.ts +0 -422
- package/src/types/request-response-augmentation.ts +0 -489
- package/src/types/route-inference.ts +0 -372
- package/src/types.ts +0 -2123
- package/src/url.ts +0 -131
- package/src/utils/index.ts +0 -1
- package/src/utils/query-preservation.ts +0 -201
- package/src/utils.ts +0 -395
- package/src/validation/validator.ts +0 -688
- package/src/websocket/clustering.ts +0 -769
package/src/index.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stacksjs/bun-router — a fast, type-safe router for Bun.
|
|
3
|
-
*
|
|
4
|
-
* Main entry point. The most commonly used exports:
|
|
5
|
-
* - `Router` — the primary router class (from `./router`)
|
|
6
|
-
* - `FluentRouter` / `router` — chainable routing API
|
|
7
|
-
* - `Auth`, `JWT`, `ApiKeyManager`, `OAuth2Helper` — auth helpers
|
|
8
|
-
* - `Container`, `createContainer` — dependency-injection container
|
|
9
|
-
* - `url()` / `registerNamedRoute()` — named-route URL generation
|
|
10
|
-
* - `Errors` — namespaced error/exception types
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// Authentication (JWT, API keys, OAuth2)
|
|
14
|
-
export { default as Auth } from './auth'
|
|
15
|
-
export * from './auth'
|
|
16
|
-
|
|
17
|
-
// Runtime configuration
|
|
18
|
-
export * from './config'
|
|
19
|
-
|
|
20
|
-
// Dependency-injection container. Exported by name — the decorator module
|
|
21
|
-
// (`Get`, `Post`, `Controller`, …) would collide with router exports if
|
|
22
|
-
// star-exported; import those from `@stacksjs/bun-router/container/decorators`.
|
|
23
|
-
export {
|
|
24
|
-
BindingBuilder,
|
|
25
|
-
Container,
|
|
26
|
-
createContainer,
|
|
27
|
-
getContainer,
|
|
28
|
-
setContainer,
|
|
29
|
-
} from './container/container'
|
|
30
|
-
export type { Binding, BindingScope, ContainerOptions, ResolutionContext, Token } from './container/container'
|
|
31
|
-
export { ContextualContainer, EnvironmentManager } from './container/contextual-binding'
|
|
32
|
-
export { DefaultServiceProviderManager } from './container/service-provider'
|
|
33
|
-
|
|
34
|
-
// Error types and helpers (namespaced — many generic names)
|
|
35
|
-
export * as Errors from './errors'
|
|
36
|
-
|
|
37
|
-
// Built-in middleware (CORS, CSRF, sessions, rate limiting, …)
|
|
38
|
-
export * from './middleware'
|
|
39
|
-
|
|
40
|
-
// Request context (AsyncLocalStorage-backed current request; lazy — see enableRequestContext)
|
|
41
|
-
export { enableRequestContext, getCurrentRequest, request, runWithRequest, setCurrentRequest } from './request/context'
|
|
42
|
-
|
|
43
|
-
// Session stores and manager
|
|
44
|
-
export * from './session'
|
|
45
|
-
|
|
46
|
-
// Response factory helpers
|
|
47
|
-
export * from './response/response-factory'
|
|
48
|
-
|
|
49
|
-
// The Router class and routing features (fluent API, throttling, caching, …)
|
|
50
|
-
export * from './router'
|
|
51
|
-
export { MiddlewareGroupRegistry, middlewareGroups } from './router/middleware-groups'
|
|
52
|
-
|
|
53
|
-
// Testing utilities
|
|
54
|
-
export * from './testing'
|
|
55
|
-
|
|
56
|
-
// Public types
|
|
57
|
-
export * from './types'
|
|
58
|
-
|
|
59
|
-
// Validation (Laravel-style rules, custom rules, middleware)
|
|
60
|
-
export * from './validation/validator'
|
|
61
|
-
|
|
62
|
-
// Named-route URL generation
|
|
63
|
-
export { clearNamedRoutes, getNamedRoutePath, getNamedRoutes, registerNamedRoute, url, type UrlOptions } from './url'
|
|
64
|
-
|
|
65
|
-
// Path/template utilities
|
|
66
|
-
export * from './utils'
|
package/src/middleware/auth.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
}
|
package/src/middleware/cors.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { EnhancedRequest, NextFunction } from '../types'
|
|
2
|
-
import { config } from '../config'
|
|
3
|
-
|
|
4
|
-
// Default CORS headers.
|
|
5
|
-
//
|
|
6
|
-
// IMPORTANT: when credentials are NOT enabled it is safe to allow any origin
|
|
7
|
-
// with `*`. The default config below deliberately keeps credentials disabled.
|
|
8
|
-
// The `*` + `Allow-Credentials: true` combination is forbidden by the CORS
|
|
9
|
-
// spec and is dangerous (it lets any site read credentialed responses), so the
|
|
10
|
-
// logic in `getCorsHeaders` guarantees that combination can never be emitted.
|
|
11
|
-
const DEFAULT_CORS_HEADERS: Record<string, string> = {
|
|
12
|
-
'Access-Control-Allow-Origin': '*',
|
|
13
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
14
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Accept, Origin',
|
|
15
|
-
'Access-Control-Max-Age': '86400',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default class Cors {
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the `Access-Control-Allow-Origin` value for a request.
|
|
21
|
-
*
|
|
22
|
-
* Supports a string origin (`'*'` or an explicit origin) or an array of
|
|
23
|
-
* allowed origins. For an allowlist, the request `Origin` is reflected back
|
|
24
|
-
* only when it is present in the list. When credentials are enabled a literal
|
|
25
|
-
* `*` is never returned (it would be both spec-violating and dangerous).
|
|
26
|
-
*/
|
|
27
|
-
private resolveAllowOrigin(
|
|
28
|
-
configuredOrigin: string | string[] | undefined,
|
|
29
|
-
credentials: boolean,
|
|
30
|
-
requestOrigin: string | null,
|
|
31
|
-
): { value: string | null, vary: boolean } {
|
|
32
|
-
// Explicit allowlist: only reflect the request origin when it matches.
|
|
33
|
-
if (Array.isArray(configuredOrigin)) {
|
|
34
|
-
if (requestOrigin && configuredOrigin.includes(requestOrigin)) {
|
|
35
|
-
return { value: requestOrigin, vary: true }
|
|
36
|
-
}
|
|
37
|
-
// Origin not allowed: do not emit an Allow-Origin header at all.
|
|
38
|
-
return { value: null, vary: true }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const origin = configuredOrigin || '*'
|
|
42
|
-
|
|
43
|
-
if (origin === '*') {
|
|
44
|
-
// The `*` wildcard cannot be combined with credentials. When credentials
|
|
45
|
-
// are enabled, reflect the specific request origin instead (which is the
|
|
46
|
-
// only spec-compliant way to support credentialed cross-origin requests).
|
|
47
|
-
if (credentials) {
|
|
48
|
-
if (requestOrigin) {
|
|
49
|
-
return { value: requestOrigin, vary: true }
|
|
50
|
-
}
|
|
51
|
-
// No request origin to reflect — omit the header rather than send `*`.
|
|
52
|
-
return { value: null, vary: true }
|
|
53
|
-
}
|
|
54
|
-
return { value: '*', vary: false }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// A single explicit origin was configured.
|
|
58
|
-
return { value: origin, vary: false }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private getCorsHeaders(req: EnhancedRequest): Record<string, string> {
|
|
62
|
-
const corsConfig = config.server?.cors
|
|
63
|
-
const requestOrigin = req.headers.get('origin')
|
|
64
|
-
|
|
65
|
-
if (corsConfig && corsConfig.enabled) {
|
|
66
|
-
const credentials = Boolean(corsConfig.credentials)
|
|
67
|
-
const { value: allowOrigin, vary } = this.resolveAllowOrigin(corsConfig.origin, credentials, requestOrigin)
|
|
68
|
-
|
|
69
|
-
const headers: Record<string, string> = {
|
|
70
|
-
'Access-Control-Allow-Methods': Array.isArray(corsConfig.methods) ? corsConfig.methods.join(', ') : DEFAULT_CORS_HEADERS['Access-Control-Allow-Methods'],
|
|
71
|
-
'Access-Control-Allow-Headers': Array.isArray(corsConfig.allowedHeaders) ? corsConfig.allowedHeaders.join(', ') : DEFAULT_CORS_HEADERS['Access-Control-Allow-Headers'],
|
|
72
|
-
'Access-Control-Max-Age': String(corsConfig.maxAge || 86400),
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (allowOrigin !== null) {
|
|
76
|
-
headers['Access-Control-Allow-Origin'] = allowOrigin
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Only advertise credentials when an origin is actually allowed. Never
|
|
80
|
-
// pair credentials with a `*` wildcard.
|
|
81
|
-
if (credentials && allowOrigin !== null && allowOrigin !== '*') {
|
|
82
|
-
headers['Access-Control-Allow-Credentials'] = 'true'
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (vary) {
|
|
86
|
-
headers.Vary = 'Origin'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return headers
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// No CORS config: fall back to the permissive (but credential-free) defaults.
|
|
93
|
-
return { ...DEFAULT_CORS_HEADERS }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
97
|
-
const corsHeaders = this.getCorsHeaders(req)
|
|
98
|
-
|
|
99
|
-
// Handle preflight OPTIONS request
|
|
100
|
-
if (req.method === 'OPTIONS') {
|
|
101
|
-
return new Response(null, { status: 204, headers: corsHeaders })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Helper to add CORS headers to any response
|
|
105
|
-
const addCorsHeaders = (response: Response): Response => {
|
|
106
|
-
const newHeaders = new Headers(response.headers)
|
|
107
|
-
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
108
|
-
newHeaders.set(key, value)
|
|
109
|
-
})
|
|
110
|
-
return new Response(response.body, {
|
|
111
|
-
status: response.status,
|
|
112
|
-
statusText: response.statusText,
|
|
113
|
-
headers: newHeaders,
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
// For non-OPTIONS requests, add CORS headers to the response
|
|
119
|
-
const response = await next()
|
|
120
|
-
if (!response) {
|
|
121
|
-
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
122
|
-
status: 404,
|
|
123
|
-
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Add CORS headers to the response
|
|
128
|
-
return addCorsHeaders(response)
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
// Error occurred - return error response WITH CORS headers
|
|
132
|
-
console.error('[CORS] Error in middleware chain:', error)
|
|
133
|
-
return new Response(JSON.stringify({
|
|
134
|
-
error: 'Internal Server Error',
|
|
135
|
-
message: error instanceof Error ? error.message : String(error),
|
|
136
|
-
}), {
|
|
137
|
-
status: 500,
|
|
138
|
-
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|