@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/request/macros.ts
DELETED
|
@@ -1,822 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request/Response Enhancements - Request Macros
|
|
3
|
-
*
|
|
4
|
-
* Laravel-style request macros for enhanced request functionality
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { EnhancedRequest } from '../types'
|
|
8
|
-
|
|
9
|
-
export interface RequestMacro {
|
|
10
|
-
name: string
|
|
11
|
-
handler: (this: EnhancedRequest, ...args: any[]) => any
|
|
12
|
-
/**
|
|
13
|
-
* Accessor macros are installed as getters on the macro prototype —
|
|
14
|
-
* `req.<name>` invokes the handler instead of returning a function.
|
|
15
|
-
* Assignment shadows the accessor with an own property.
|
|
16
|
-
*/
|
|
17
|
-
accessor?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Hoisted hot-path helpers — compiled once instead of per call
|
|
21
|
-
const MOBILE_REGEX = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
|
|
22
|
-
const BOT_REGEX = /bot|crawler|spider|crawling/i
|
|
23
|
-
const GLOB_PATTERN_CACHE = new Map<string, RegExp>()
|
|
24
|
-
|
|
25
|
-
/** Parsed-state cache slots attached to a request on first use. */
|
|
26
|
-
interface RequestParseCache {
|
|
27
|
-
_parsedURL?: URL
|
|
28
|
-
_parsedCookies?: Record<string, string>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getParsedURL(req: EnhancedRequest): URL {
|
|
32
|
-
const cache = req as EnhancedRequest & RequestParseCache
|
|
33
|
-
if (!cache._parsedURL) {
|
|
34
|
-
cache._parsedURL = new URL(req.url)
|
|
35
|
-
}
|
|
36
|
-
return cache._parsedURL
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getParsedCookies(req: EnhancedRequest): Record<string, string> {
|
|
40
|
-
const cache = req as EnhancedRequest & RequestParseCache
|
|
41
|
-
if (!cache._parsedCookies) {
|
|
42
|
-
const cookies: Record<string, string> = {}
|
|
43
|
-
const cookieHeader = req.headers.get('cookie')
|
|
44
|
-
if (cookieHeader) {
|
|
45
|
-
for (const cookie of cookieHeader.split(';')) {
|
|
46
|
-
const eqIndex = cookie.indexOf('=')
|
|
47
|
-
if (eqIndex === -1)
|
|
48
|
-
continue
|
|
49
|
-
const key = cookie.slice(0, eqIndex).trim()
|
|
50
|
-
if (!key)
|
|
51
|
-
continue
|
|
52
|
-
const value = cookie.slice(eqIndex + 1).trim()
|
|
53
|
-
try {
|
|
54
|
-
cookies[key] = decodeURIComponent(value)
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
cookies[key] = value
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
cache._parsedCookies = cookies
|
|
62
|
-
}
|
|
63
|
-
return cache._parsedCookies
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Request macro registry
|
|
68
|
-
*/
|
|
69
|
-
class RequestMacroRegistry {
|
|
70
|
-
private macros: Map<string, RequestMacro> = new Map()
|
|
71
|
-
// Bumped on every mutation so cached macro prototypes can be rebuilt
|
|
72
|
-
private epoch = 0
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Register a request macro
|
|
76
|
-
*/
|
|
77
|
-
register(name: string, handler: (this: EnhancedRequest, ...args: any[]) => any, accessor = false): void {
|
|
78
|
-
this.macros.set(name, { name, handler, accessor })
|
|
79
|
-
this.epoch++
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get a registered macro
|
|
84
|
-
*/
|
|
85
|
-
get(name: string): RequestMacro | undefined {
|
|
86
|
-
return this.macros.get(name)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Check if macro exists
|
|
91
|
-
*/
|
|
92
|
-
has(name: string): boolean {
|
|
93
|
-
return this.macros.has(name)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get all registered macros
|
|
98
|
-
*/
|
|
99
|
-
all(): RequestMacro[] {
|
|
100
|
-
return Array.from(this.macros.values())
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Remove a macro
|
|
105
|
-
*/
|
|
106
|
-
remove(name: string): boolean {
|
|
107
|
-
const removed = this.macros.delete(name)
|
|
108
|
-
if (removed)
|
|
109
|
-
this.epoch++
|
|
110
|
-
return removed
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Clear all macros
|
|
115
|
-
*/
|
|
116
|
-
clear(): void {
|
|
117
|
-
this.macros.clear()
|
|
118
|
-
this.epoch++
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Monotonic registry version (see `RequestWithMacros.applyMacros`)
|
|
123
|
-
*/
|
|
124
|
-
getEpoch(): number {
|
|
125
|
-
return this.epoch
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Global request macro registry
|
|
131
|
-
*/
|
|
132
|
-
export const requestMacroRegistry: RequestMacroRegistry = new RequestMacroRegistry()
|
|
133
|
-
|
|
134
|
-
// Marker symbols for prototype-based macro attachment
|
|
135
|
-
const MACRO_PROTO = Symbol('bunRouterMacroProto')
|
|
136
|
-
const MACRO_BASE = Symbol('bunRouterMacroBase')
|
|
137
|
-
|
|
138
|
-
// Cache of generated macro prototypes, keyed by the request's original
|
|
139
|
-
// prototype. Rebuilt whenever the registry epoch moves.
|
|
140
|
-
let macroProtoCache = new WeakMap<object, Record<PropertyKey, unknown>>()
|
|
141
|
-
let macroProtoEpoch = -1
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Request class with macro support
|
|
145
|
-
*/
|
|
146
|
-
export class RequestWithMacros {
|
|
147
|
-
/**
|
|
148
|
-
* Register a request macro
|
|
149
|
-
*/
|
|
150
|
-
static macro(name: string, handler: (this: EnhancedRequest, ...args: any[]) => any): void {
|
|
151
|
-
requestMacroRegistry.register(name, handler)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Register an accessor macro: `req.<name>` evaluates the getter
|
|
156
|
-
* (instead of exposing a callable). Assignment to the property
|
|
157
|
-
* shadows the accessor with an own value, so consumers that set
|
|
158
|
-
* `req.<name> = ...` keep working.
|
|
159
|
-
*/
|
|
160
|
-
static macroAccessor(name: string, getter: (this: EnhancedRequest) => any): void {
|
|
161
|
-
requestMacroRegistry.register(name, getter, true)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Apply macros to a request object.
|
|
166
|
-
*
|
|
167
|
-
* Macros are attached via a shared prototype inserted between the
|
|
168
|
-
* request and its original prototype — one `setPrototypeOf` per
|
|
169
|
-
* request. The previous implementation looped over every macro and
|
|
170
|
-
* `bind`-assigned it, costing ~40 property writes and closures per
|
|
171
|
-
* request on the dispatch hot path. Macro handlers receive the
|
|
172
|
-
* request as `this`, so no per-request binding is needed.
|
|
173
|
-
*/
|
|
174
|
-
static applyMacros(request: EnhancedRequest): EnhancedRequest {
|
|
175
|
-
const epoch = requestMacroRegistry.getEpoch()
|
|
176
|
-
if (epoch !== macroProtoEpoch) {
|
|
177
|
-
macroProtoCache = new WeakMap()
|
|
178
|
-
macroProtoEpoch = epoch
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let base = (Object.getPrototypeOf(request) ?? Object.prototype) as Record<PropertyKey, unknown>
|
|
182
|
-
if (base[MACRO_PROTO]) {
|
|
183
|
-
// Request was already enhanced. Current generation: nothing to do.
|
|
184
|
-
if (macroProtoCache.get(base[MACRO_BASE] as object) === base) {
|
|
185
|
-
return request
|
|
186
|
-
}
|
|
187
|
-
// Stale generation: rebuild on top of the original prototype
|
|
188
|
-
base = base[MACRO_BASE] as Record<PropertyKey, unknown>
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let proto = macroProtoCache.get(base)
|
|
192
|
-
if (!proto) {
|
|
193
|
-
proto = Object.create(base) as Record<PropertyKey, unknown>
|
|
194
|
-
for (const macro of requestMacroRegistry.all()) {
|
|
195
|
-
if (macro.accessor) {
|
|
196
|
-
Object.defineProperty(proto, macro.name, {
|
|
197
|
-
get: macro.handler,
|
|
198
|
-
// Assignment shadows the accessor with an own property
|
|
199
|
-
set(value: unknown) {
|
|
200
|
-
Object.defineProperty(this, macro.name, {
|
|
201
|
-
value,
|
|
202
|
-
writable: true,
|
|
203
|
-
configurable: true,
|
|
204
|
-
enumerable: true,
|
|
205
|
-
})
|
|
206
|
-
},
|
|
207
|
-
configurable: true,
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
proto[macro.name] = macro.handler
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
Object.defineProperty(proto, MACRO_PROTO, { value: true })
|
|
215
|
-
Object.defineProperty(proto, MACRO_BASE, { value: base })
|
|
216
|
-
macroProtoCache.set(base, proto)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
Object.setPrototypeOf(request, proto)
|
|
220
|
-
return request
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Check if macro exists
|
|
225
|
-
*/
|
|
226
|
-
static hasMacro(name: string): boolean {
|
|
227
|
-
return requestMacroRegistry.has(name)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Built-in request macros
|
|
233
|
-
*/
|
|
234
|
-
export const BuiltInRequestMacros = {
|
|
235
|
-
/**
|
|
236
|
-
* Check if request wants JSON response
|
|
237
|
-
*/
|
|
238
|
-
wantsJson(this: EnhancedRequest): boolean {
|
|
239
|
-
const accept = this.headers.get('accept') || ''
|
|
240
|
-
return accept.includes('application/json') || accept.includes('json')
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Check if request wants HTML response
|
|
245
|
-
*/
|
|
246
|
-
wantsHtml(this: EnhancedRequest): boolean {
|
|
247
|
-
const accept = this.headers.get('accept') || ''
|
|
248
|
-
return accept.includes('text/html') || accept.includes('html')
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Check if request wants XML response
|
|
253
|
-
*/
|
|
254
|
-
wantsXml(this: EnhancedRequest): boolean {
|
|
255
|
-
const accept = this.headers.get('accept') || ''
|
|
256
|
-
return accept.includes('application/xml') || accept.includes('xml')
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Check if request expects JSON response
|
|
261
|
-
*/
|
|
262
|
-
expectsJson(this: EnhancedRequest): boolean {
|
|
263
|
-
return this.wantsJson() || this.isAjax() || this.isPjax()
|
|
264
|
-
},
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Check if request is AJAX
|
|
268
|
-
*/
|
|
269
|
-
isAjax(this: EnhancedRequest): boolean {
|
|
270
|
-
return this.headers.get('x-requested-with') === 'XMLHttpRequest'
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Check if request is PJAX
|
|
275
|
-
*/
|
|
276
|
-
isPjax(this: EnhancedRequest): boolean {
|
|
277
|
-
return this.headers.get('x-pjax') === 'true'
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check if request is from mobile device
|
|
282
|
-
*/
|
|
283
|
-
isMobile(this: EnhancedRequest): boolean {
|
|
284
|
-
return MOBILE_REGEX.test(this.headers.get('user-agent') || '')
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Check if request is from bot/crawler
|
|
289
|
-
*/
|
|
290
|
-
isBot(this: EnhancedRequest): boolean {
|
|
291
|
-
return BOT_REGEX.test(this.headers.get('user-agent') || '')
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Check if request is secure (HTTPS)
|
|
296
|
-
*/
|
|
297
|
-
isSecure(this: EnhancedRequest): boolean {
|
|
298
|
-
return this.url.startsWith('https://')
|
|
299
|
-
|| this.headers.get('x-forwarded-proto') === 'https'
|
|
300
|
-
|| this.headers.get('x-forwarded-ssl') === 'on'
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Get client IP address
|
|
305
|
-
*/
|
|
306
|
-
ip(this: EnhancedRequest): string {
|
|
307
|
-
return this.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
|
308
|
-
|| this.headers.get('x-real-ip')
|
|
309
|
-
|| this.headers.get('cf-connecting-ip')
|
|
310
|
-
|| this.headers.get('x-client-ip')
|
|
311
|
-
|| 'unknown'
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Get all client IPs (including proxies)
|
|
316
|
-
*/
|
|
317
|
-
ips(this: EnhancedRequest): string[] {
|
|
318
|
-
const forwardedFor = this.headers.get('x-forwarded-for')
|
|
319
|
-
if (forwardedFor) {
|
|
320
|
-
return forwardedFor.split(',').map(ip => ip.trim())
|
|
321
|
-
}
|
|
322
|
-
return [BuiltInRequestMacros.ip.call(this)]
|
|
323
|
-
},
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Get user agent
|
|
327
|
-
*/
|
|
328
|
-
userAgent(this: EnhancedRequest): string {
|
|
329
|
-
return this.headers.get('user-agent') || ''
|
|
330
|
-
},
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Get referer URL
|
|
334
|
-
*/
|
|
335
|
-
referer(this: EnhancedRequest): string | null {
|
|
336
|
-
return this.headers.get('referer') || this.headers.get('referrer') || null
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Get authorization header
|
|
341
|
-
*/
|
|
342
|
-
bearerToken(this: EnhancedRequest): string | null {
|
|
343
|
-
const auth = this.headers.get('authorization')
|
|
344
|
-
if (auth && auth.startsWith('Bearer ')) {
|
|
345
|
-
return auth.substring(7)
|
|
346
|
-
}
|
|
347
|
-
return null
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Get the raw, unparsed request body as a string.
|
|
352
|
-
*
|
|
353
|
-
* The body stream can only be consumed once, so this caches the result on
|
|
354
|
-
* `_rawBody`. Signature-verifying callbacks (Stripe/GitHub/Slack webhooks)
|
|
355
|
-
* need the exact bytes the client sent, before any JSON parsing — a parsed
|
|
356
|
-
* `jsonBody` re-serialized is NOT byte-identical and will fail HMAC checks.
|
|
357
|
-
* A framework body parser that has already read the body may populate
|
|
358
|
-
* `_rawBody` up front so this returns without re-reading.
|
|
359
|
-
*/
|
|
360
|
-
async rawBody(this: EnhancedRequest): Promise<string> {
|
|
361
|
-
if (typeof this._rawBody === 'string')
|
|
362
|
-
return this._rawBody
|
|
363
|
-
try {
|
|
364
|
-
const text = await this.clone().text()
|
|
365
|
-
this._rawBody = text
|
|
366
|
-
return text
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
return ''
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Get basic auth credentials
|
|
375
|
-
*/
|
|
376
|
-
basicAuth(this: EnhancedRequest): { username: string, password: string } | null {
|
|
377
|
-
const auth = this.headers.get('authorization')
|
|
378
|
-
if (auth && auth.startsWith('Basic ')) {
|
|
379
|
-
try {
|
|
380
|
-
const decoded = atob(auth.substring(6))
|
|
381
|
-
const [username, password] = decoded.split(':')
|
|
382
|
-
return { username, password }
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
return null
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return null
|
|
389
|
-
},
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Check if request has specific header
|
|
393
|
-
*/
|
|
394
|
-
hasHeader(this: EnhancedRequest, name: string): boolean {
|
|
395
|
-
return this.headers.has(name.toLowerCase())
|
|
396
|
-
},
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Get header value with default
|
|
400
|
-
*/
|
|
401
|
-
header(this: EnhancedRequest, name: string, defaultValue?: string): string | null {
|
|
402
|
-
return this.headers.get(name.toLowerCase()) || defaultValue || null
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Get all headers as object
|
|
407
|
-
*/
|
|
408
|
-
allHeaders(this: EnhancedRequest): Record<string, string> {
|
|
409
|
-
const headers: Record<string, string> = {}
|
|
410
|
-
this.headers.forEach((value, key) => {
|
|
411
|
-
headers[key] = value
|
|
412
|
-
})
|
|
413
|
-
return headers
|
|
414
|
-
},
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Get input value from query, body, or params
|
|
418
|
-
*/
|
|
419
|
-
input(this: EnhancedRequest, key: string, defaultValue?: any): any {
|
|
420
|
-
// Check validated data first
|
|
421
|
-
if (this.validated && key in this.validated) {
|
|
422
|
-
return this.validated[key]
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Check query parameters
|
|
426
|
-
if (this.query && key in this.query) {
|
|
427
|
-
return this.query[key]
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Check route parameters
|
|
431
|
-
if (this.params && key in this.params) {
|
|
432
|
-
return this.params[key]
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Check JSON body
|
|
436
|
-
if (this.jsonBody && typeof this.jsonBody === 'object' && key in this.jsonBody) {
|
|
437
|
-
return (this.jsonBody as Record<string, unknown>)[key]
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Check form body
|
|
441
|
-
if (this.formBody && typeof this.formBody === 'object' && key in this.formBody) {
|
|
442
|
-
return (this.formBody as Record<string, unknown>)[key]
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return defaultValue
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Get all input data
|
|
450
|
-
*/
|
|
451
|
-
all(this: EnhancedRequest): Record<string, unknown> {
|
|
452
|
-
return {
|
|
453
|
-
...this.params,
|
|
454
|
-
...this.query,
|
|
455
|
-
...(typeof this.jsonBody === 'object' && this.jsonBody !== null ? this.jsonBody : {}),
|
|
456
|
-
...(typeof this.formBody === 'object' && this.formBody !== null ? this.formBody : {}),
|
|
457
|
-
...(typeof this.validated === 'object' && this.validated !== null ? this.validated : {}),
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Get only specified input keys
|
|
463
|
-
*/
|
|
464
|
-
only(this: EnhancedRequest, keys: string[]): Record<string, any> {
|
|
465
|
-
const all = this.all()
|
|
466
|
-
const result: Record<string, any> = {}
|
|
467
|
-
|
|
468
|
-
keys.forEach((key) => {
|
|
469
|
-
if (key in all) {
|
|
470
|
-
result[key] = all[key]
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
return result
|
|
475
|
-
},
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Get all input except specified keys
|
|
479
|
-
*/
|
|
480
|
-
except(this: EnhancedRequest, keys: string[]): Record<string, any> {
|
|
481
|
-
const all = this.all()
|
|
482
|
-
const result: Record<string, any> = {}
|
|
483
|
-
|
|
484
|
-
Object.keys(all).forEach((key) => {
|
|
485
|
-
if (!keys.includes(key)) {
|
|
486
|
-
result[key] = all[key]
|
|
487
|
-
}
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
return result
|
|
491
|
-
},
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Check if input has specific key
|
|
495
|
-
*/
|
|
496
|
-
has(this: EnhancedRequest, key: string): boolean {
|
|
497
|
-
const all = this.all()
|
|
498
|
-
return key in all && all[key] !== undefined && all[key] !== null && all[key] !== ''
|
|
499
|
-
},
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Check if input has any of the specified keys
|
|
503
|
-
*/
|
|
504
|
-
hasAny(this: EnhancedRequest, keys: string[]): boolean {
|
|
505
|
-
return keys.some(key => this.has(key))
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Check if input is missing specific key
|
|
510
|
-
*/
|
|
511
|
-
missing(this: EnhancedRequest, key: string): boolean {
|
|
512
|
-
return !this.has(key)
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Check if input is filled (not empty)
|
|
517
|
-
*/
|
|
518
|
-
filled(this: EnhancedRequest, key: string): boolean {
|
|
519
|
-
const value = this.input(key)
|
|
520
|
-
if (value === null || value === undefined)
|
|
521
|
-
return false
|
|
522
|
-
if (typeof value === 'string')
|
|
523
|
-
return value.trim().length > 0
|
|
524
|
-
if (Array.isArray(value))
|
|
525
|
-
return value.length > 0
|
|
526
|
-
if (typeof value === 'object')
|
|
527
|
-
return Object.keys(value).length > 0
|
|
528
|
-
return true
|
|
529
|
-
},
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Get query parameter
|
|
533
|
-
*/
|
|
534
|
-
getQuery(this: EnhancedRequest, key?: string, defaultValue?: any): any {
|
|
535
|
-
if (!key)
|
|
536
|
-
return this.query || {}
|
|
537
|
-
return this.query?.[key] || defaultValue
|
|
538
|
-
},
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Get route parameter
|
|
542
|
-
*/
|
|
543
|
-
param(this: EnhancedRequest, key: string, defaultValue?: any): any {
|
|
544
|
-
return this.params?.[key] || defaultValue
|
|
545
|
-
},
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Get cookie value
|
|
549
|
-
*/
|
|
550
|
-
cookie(this: EnhancedRequest, name: string, defaultValue?: string): string | null {
|
|
551
|
-
const value = getParsedCookies(this)[name]
|
|
552
|
-
return value !== undefined ? value : (defaultValue || null)
|
|
553
|
-
},
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Get all cookies
|
|
557
|
-
*/
|
|
558
|
-
cookies(this: EnhancedRequest): Record<string, string> {
|
|
559
|
-
return { ...getParsedCookies(this) }
|
|
560
|
-
},
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Get file from upload
|
|
564
|
-
*/
|
|
565
|
-
file(this: EnhancedRequest, name: string): any {
|
|
566
|
-
return this.files?.find(f => f.fieldName === name)
|
|
567
|
-
},
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Check if request has file upload
|
|
571
|
-
*/
|
|
572
|
-
hasFile(this: EnhancedRequest, name: string): boolean {
|
|
573
|
-
return !!this.files?.find(f => f.fieldName === name)
|
|
574
|
-
},
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Get request path without query string
|
|
578
|
-
*/
|
|
579
|
-
path(this: EnhancedRequest): string {
|
|
580
|
-
return getParsedURL(this).pathname
|
|
581
|
-
},
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Get request URL
|
|
585
|
-
*/
|
|
586
|
-
fullUrl(this: EnhancedRequest): string {
|
|
587
|
-
return this.url
|
|
588
|
-
},
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Get request root URL
|
|
592
|
-
*/
|
|
593
|
-
root(this: EnhancedRequest): string {
|
|
594
|
-
const url = getParsedURL(this)
|
|
595
|
-
return `${url.protocol}//${url.host}`
|
|
596
|
-
},
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Check if path matches pattern
|
|
600
|
-
*/
|
|
601
|
-
is(this: EnhancedRequest, pattern: string): boolean {
|
|
602
|
-
const path = this.path()
|
|
603
|
-
|
|
604
|
-
// Compiled glob patterns are memoized — route checks tend to reuse a
|
|
605
|
-
// small set of patterns across many requests
|
|
606
|
-
let regex = GLOB_PATTERN_CACHE.get(pattern)
|
|
607
|
-
if (!regex) {
|
|
608
|
-
const regexPattern = pattern
|
|
609
|
-
.replace(/\*/g, '.*')
|
|
610
|
-
.replace(/\?/g, '.')
|
|
611
|
-
regex = new RegExp(`^${regexPattern}$`)
|
|
612
|
-
if (GLOB_PATTERN_CACHE.size >= 1000) {
|
|
613
|
-
GLOB_PATTERN_CACHE.clear()
|
|
614
|
-
}
|
|
615
|
-
GLOB_PATTERN_CACHE.set(pattern, regex)
|
|
616
|
-
}
|
|
617
|
-
return regex.test(path)
|
|
618
|
-
},
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Get route name (if available)
|
|
622
|
-
*/
|
|
623
|
-
route(this: EnhancedRequest): string | null {
|
|
624
|
-
return (this as any).routeName || null
|
|
625
|
-
},
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Get request fingerprint for caching
|
|
629
|
-
*/
|
|
630
|
-
fingerprint(this: EnhancedRequest): string {
|
|
631
|
-
const url = getParsedURL(this)
|
|
632
|
-
const data = {
|
|
633
|
-
method: this.method,
|
|
634
|
-
path: url.pathname,
|
|
635
|
-
query: url.search,
|
|
636
|
-
ip: BuiltInRequestMacros.ip.call(this),
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return btoa(JSON.stringify(data))
|
|
640
|
-
},
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Get request signature for security
|
|
644
|
-
*/
|
|
645
|
-
signature(this: EnhancedRequest, secret: string): string {
|
|
646
|
-
const data = `${this.method}${this.url}${BuiltInRequestMacros.ip.call(this)}${secret}`
|
|
647
|
-
|
|
648
|
-
// Simple hash function (in production, use crypto)
|
|
649
|
-
let hash = 0
|
|
650
|
-
for (let i = 0; i < data.length; i++) {
|
|
651
|
-
const char = data.charCodeAt(i)
|
|
652
|
-
hash = ((hash << 5) - hash) + char
|
|
653
|
-
hash = hash & hash // Convert to 32-bit integer
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
return Math.abs(hash).toString(36)
|
|
657
|
-
},
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Merge additional data into request
|
|
661
|
-
*/
|
|
662
|
-
merge(this: EnhancedRequest, data: Record<string, any>): void {
|
|
663
|
-
if (!this.validated) {
|
|
664
|
-
this.validated = {}
|
|
665
|
-
}
|
|
666
|
-
Object.assign(this.validated, data)
|
|
667
|
-
},
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Replace request data
|
|
671
|
-
*/
|
|
672
|
-
replace(this: EnhancedRequest, data: Record<string, any>): void {
|
|
673
|
-
this.validated = { ...data }
|
|
674
|
-
},
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Get request age in milliseconds
|
|
678
|
-
*/
|
|
679
|
-
age(this: EnhancedRequest): number {
|
|
680
|
-
const startTime = (this as any).startTime || Date.now()
|
|
681
|
-
return Date.now() - startTime
|
|
682
|
-
},
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Check if request is from trusted proxy
|
|
686
|
-
*/
|
|
687
|
-
isFromTrustedProxy(this: EnhancedRequest, trustedProxies: string[] = []): boolean {
|
|
688
|
-
const ip = BuiltInRequestMacros.ip.call(this)
|
|
689
|
-
return trustedProxies.includes(ip)
|
|
690
|
-
},
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Get request content length
|
|
694
|
-
*/
|
|
695
|
-
contentLength(this: EnhancedRequest): number {
|
|
696
|
-
const length = this.headers.get('content-length')
|
|
697
|
-
return length ? Number.parseInt(length, 10) : 0
|
|
698
|
-
},
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Get request content type
|
|
702
|
-
*/
|
|
703
|
-
contentType(this: EnhancedRequest): string | null {
|
|
704
|
-
return this.headers.get('content-type')
|
|
705
|
-
},
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Check if request content type matches
|
|
709
|
-
*/
|
|
710
|
-
isContentType(this: EnhancedRequest, type: string): boolean {
|
|
711
|
-
const contentType = this.contentType()
|
|
712
|
-
return contentType ? contentType.includes(type) : false
|
|
713
|
-
},
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Register all built-in macros
|
|
718
|
-
*/
|
|
719
|
-
export function registerBuiltInRequestMacros(): void {
|
|
720
|
-
Object.entries(BuiltInRequestMacros).forEach(([name, handler]) => {
|
|
721
|
-
RequestWithMacros.macro(name, handler)
|
|
722
|
-
})
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Request macro factory functions
|
|
727
|
-
*/
|
|
728
|
-
export const RequestMacroFactory = {
|
|
729
|
-
/**
|
|
730
|
-
* Create input validation macro
|
|
731
|
-
*/
|
|
732
|
-
validateInput: (_rules: Record<string, string>) => {
|
|
733
|
-
// eslint-disable-next-line pickier/no-unused-vars
|
|
734
|
-
return function (this: EnhancedRequest): boolean {
|
|
735
|
-
// This would integrate with the validation system
|
|
736
|
-
// For now, return true as placeholder
|
|
737
|
-
return true
|
|
738
|
-
}
|
|
739
|
-
},
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Create custom header checker macro
|
|
743
|
-
*/
|
|
744
|
-
hasCustomHeader: (headerName: string) => {
|
|
745
|
-
return function (this: EnhancedRequest): boolean {
|
|
746
|
-
return this.hasHeader(headerName)
|
|
747
|
-
}
|
|
748
|
-
},
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* Create role checker macro
|
|
752
|
-
*/
|
|
753
|
-
hasRole: (role: string) => {
|
|
754
|
-
return function (this: EnhancedRequest): boolean {
|
|
755
|
-
const user = (this as any).user
|
|
756
|
-
return user?.roles?.includes(role) || false
|
|
757
|
-
}
|
|
758
|
-
},
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Create permission checker macro
|
|
762
|
-
*/
|
|
763
|
-
can: (permission: string) => {
|
|
764
|
-
return function (this: EnhancedRequest): boolean {
|
|
765
|
-
const user = (this as any).user
|
|
766
|
-
return user?.permissions?.includes(permission) || false
|
|
767
|
-
}
|
|
768
|
-
},
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Request helper utilities
|
|
773
|
-
*/
|
|
774
|
-
export const RequestHelpers = {
|
|
775
|
-
/**
|
|
776
|
-
* Apply macros to request
|
|
777
|
-
*/
|
|
778
|
-
withMacros: (request: EnhancedRequest): EnhancedRequest => {
|
|
779
|
-
return RequestWithMacros.applyMacros(request)
|
|
780
|
-
},
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Create request from URL and options
|
|
784
|
-
*/
|
|
785
|
-
create: (url: string, options: RequestInit = {}): EnhancedRequest => {
|
|
786
|
-
const request = new Request(url, options) as EnhancedRequest
|
|
787
|
-
return RequestWithMacros.applyMacros(request)
|
|
788
|
-
},
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Parse query string to object
|
|
792
|
-
*/
|
|
793
|
-
parseQuery: (queryString: string): Record<string, string> => {
|
|
794
|
-
const params = new URLSearchParams(queryString)
|
|
795
|
-
const result: Record<string, string> = {}
|
|
796
|
-
|
|
797
|
-
params.forEach((value, key) => {
|
|
798
|
-
result[key] = value
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
return result
|
|
802
|
-
},
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Parse cookies from header
|
|
806
|
-
*/
|
|
807
|
-
parseCookies: (cookieHeader: string): Record<string, string> => {
|
|
808
|
-
const cookies: Record<string, string> = {}
|
|
809
|
-
|
|
810
|
-
cookieHeader.split(';').forEach((cookie) => {
|
|
811
|
-
const [key, value] = cookie.trim().split('=')
|
|
812
|
-
if (key && value) {
|
|
813
|
-
cookies[key] = decodeURIComponent(value)
|
|
814
|
-
}
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
return cookies
|
|
818
|
-
},
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Auto-register built-in macros
|
|
822
|
-
registerBuiltInRequestMacros()
|