@stacksjs/bun-router 0.0.15 → 0.0.17
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/auth.d.ts +22 -8
- package/dist/chunk-1ahs68ys.js +18 -0
- package/dist/{chunk-j0e7z7hd.js → chunk-cgptvjdf.js} +153 -75
- package/dist/chunk-g3ybefhg.js +1923 -0
- package/dist/cli.js +13 -11
- package/dist/container/container.d.ts +1 -0
- package/dist/container/contextual-binding.d.ts +0 -1
- package/dist/container/decorators.d.ts +2 -5
- package/dist/container/index.d.ts +16 -0
- package/dist/container/index.js +322 -0
- package/dist/file-serving/static-files.d.ts +16 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1086 -1270
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +16 -1
- package/dist/middleware/rate_limit.d.ts +7 -1
- package/dist/middleware/request_tracer.d.ts +15 -9
- package/dist/middleware/response_cache.d.ts +2 -1
- package/dist/request/context.d.ts +12 -1
- package/dist/request/macros.d.ts +40 -2
- package/dist/response/macros.d.ts +3 -3
- package/dist/router/fluent-routing.d.ts +50 -13
- package/dist/router/handler-resolver.d.ts +8 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/route-compiler.d.ts +8 -10
- package/dist/router/route-trie.d.ts +7 -1
- package/dist/router/router.d.ts +13 -2
- package/dist/router/validation-integration.d.ts +10 -4
- package/dist/types/middleware-types.d.ts +1 -1
- package/dist/types/route-inference.d.ts +9 -2
- package/dist/types.d.ts +62 -9
- package/dist/utils.d.ts +1 -1
- package/package.json +11 -1
- package/src/auth.ts +32 -12
- package/src/container/container.ts +58 -12
- package/src/container/contextual-binding.ts +4 -1
- package/src/container/decorators.ts +16 -7
- package/src/container/index.ts +49 -0
- package/src/errors/circuit-breaker.ts +3 -2
- package/src/errors/graceful-degradation.ts +2 -0
- package/src/file-serving/static-files.ts +144 -7
- package/src/index.ts +53 -1
- package/src/middleware/cors.ts +76 -8
- package/src/middleware/csrf.ts +80 -22
- package/src/middleware/ddos_protection.ts +27 -3
- package/src/middleware/input_validation.ts +10 -6
- package/src/middleware/performance_alerting.ts +2 -0
- package/src/middleware/performance_monitor.ts +2 -0
- package/src/middleware/rate_limit.ts +7 -1
- package/src/middleware/request_signing.ts +2 -0
- package/src/middleware/request_tracer.ts +34 -17
- package/src/middleware/response_cache.ts +48 -13
- package/src/middleware/session.ts +3 -3
- package/src/observability/metrics.ts +1 -1
- package/src/request/context.ts +50 -3
- package/src/request/enhanced-request.ts +20 -1
- package/src/request/macros.ts +183 -49
- package/src/response/macros.ts +3 -3
- package/src/router/file-based-routing.ts +17 -10
- package/src/router/fluent-routing.ts +117 -48
- package/src/router/group-organization.ts +17 -1
- package/src/router/handler-resolver.ts +36 -0
- package/src/router/http-methods.ts +33 -3
- package/src/router/index.ts +6 -0
- package/src/router/middleware.ts +3 -0
- package/src/router/optimized-route-matching.ts +58 -9
- package/src/router/route-compiler.ts +62 -73
- package/src/router/route-matching.ts +19 -0
- package/src/router/route-trie.ts +34 -48
- package/src/router/router.ts +72 -60
- package/src/router/server.ts +361 -139
- package/src/router/validation-integration.ts +11 -5
- package/src/testing/auth-testing.ts +6 -4
- package/src/types/middleware-types.ts +7 -4
- package/src/types/route-inference.ts +29 -14
- package/src/types.ts +63 -7
- package/src/url.ts +7 -2
- package/src/utils.ts +107 -39
- package/src/validation/validator.ts +7 -4
- package/src/websocket/clustering.ts +8 -1
- package/dist/router/fluent-router.d.ts +0 -315
- package/src/router/fluent-router.ts +0 -927
|
@@ -4,16 +4,29 @@
|
|
|
4
4
|
* Advanced TypeScript utilities for inferring route parameter types from URL patterns
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
/** Strip an inline constraint from a brace parameter: `id:[0-9]+` → `id` */
|
|
8
|
+
type StripInlineConstraint<T extends string> = T extends `${infer Name}:${string}` ? Name : T
|
|
9
|
+
|
|
10
|
+
// Base types for route parameter extraction.
|
|
11
|
+
//
|
|
12
|
+
// The router's canonical parameter syntax is `{param}` (with `{param?}` for
|
|
13
|
+
// optional and `{param:regex}` for constrained parameters) — that form is
|
|
14
|
+
// handled first. The legacy Express-style `:param` clauses are kept for
|
|
15
|
+
// backward compatibility with code written against the colon syntax.
|
|
16
|
+
export type ExtractRouteParams<T extends string> =
|
|
17
|
+
T extends `${string}{${infer Param}}${infer Rest}`
|
|
18
|
+
? (Param extends `${infer Inner}?`
|
|
19
|
+
? { [K in StripInlineConstraint<Inner>]?: string }
|
|
20
|
+
: { [K in StripInlineConstraint<Param>]: string }) & ExtractRouteParams<Rest>
|
|
21
|
+
: T extends `${infer _Start}:${infer Param}/${infer Rest}`
|
|
22
|
+
? { [K in Param]: string } & ExtractRouteParams<`/${Rest}`>
|
|
23
|
+
: T extends `${infer _Start}:${infer Param}?${infer Rest}`
|
|
24
|
+
? { [K in Param]?: string } & ExtractRouteParams<Rest>
|
|
25
|
+
: T extends `${infer _Start}:${infer Param}`
|
|
26
|
+
? { [K in Param]: string }
|
|
27
|
+
: T extends `${infer _Start}*${infer Rest}`
|
|
28
|
+
? { '*': string } & ExtractRouteParams<Rest>
|
|
29
|
+
: Record<string, never>
|
|
17
30
|
|
|
18
31
|
// Advanced parameter extraction with type constraints
|
|
19
32
|
export type ExtractTypedParams<T extends string> = T extends `${infer _Start}:${infer Param}<${infer Type}>${infer Rest}`
|
|
@@ -214,11 +227,13 @@ export interface RouteGroup<TPrefix extends string = '', TContext = object> {
|
|
|
214
227
|
routes: TypedRoute<any, any, any, TContext>[]
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
// Middleware type (forward declaration
|
|
230
|
+
// Middleware type (forward declaration — the canonical definition lives in
|
|
231
|
+
// middleware-types.ts; this one mirrors its narrow defaults so the two
|
|
232
|
+
// can't drift apart silently)
|
|
218
233
|
export interface TypedMiddleware<
|
|
219
|
-
TRequest =
|
|
220
|
-
TResponse =
|
|
221
|
-
TNext =
|
|
234
|
+
TRequest = Request,
|
|
235
|
+
TResponse = Response,
|
|
236
|
+
TNext = () => Promise<TResponse>,
|
|
222
237
|
_TContext = Record<string, never>,
|
|
223
238
|
> {
|
|
224
239
|
(request: TRequest, next: TNext): Promise<TResponse> | TResponse
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Server } from 'bun'
|
|
2
2
|
import type { Router } from './router/router'
|
|
3
|
+
import type { SessionManager } from './session/index'
|
|
3
4
|
import type { QueryPreservationConfig } from './utils/query-preservation'
|
|
4
5
|
import type {
|
|
5
6
|
AuthContext,
|
|
@@ -807,6 +808,12 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
|
|
|
807
808
|
* Parsed JSON body (if Content-Type is application/json)
|
|
808
809
|
*/
|
|
809
810
|
jsonBody?: Record<string, unknown>
|
|
811
|
+
/**
|
|
812
|
+
* Raw unparsed request body cache, populated on first `rawBody()` read (or up
|
|
813
|
+
* front by a framework body parser). Kept so webhook signature checks get the
|
|
814
|
+
* exact bytes the client sent.
|
|
815
|
+
*/
|
|
816
|
+
_rawBody?: string
|
|
810
817
|
/**
|
|
811
818
|
* Form body data (if Content-Type is multipart/form-data or application/x-www-form-urlencoded)
|
|
812
819
|
*/
|
|
@@ -816,9 +823,11 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
|
|
|
816
823
|
*/
|
|
817
824
|
files?: UploadedFile[]
|
|
818
825
|
/**
|
|
819
|
-
* Session
|
|
826
|
+
* Session for the current request. The built-in session middleware
|
|
827
|
+
* attaches a `SessionManager` (with `get`/`set`/`flash`/… methods);
|
|
828
|
+
* testing utilities may attach plain `SessionData`.
|
|
820
829
|
*/
|
|
821
|
-
session?: SessionData
|
|
830
|
+
session?: SessionManager | SessionData
|
|
822
831
|
/**
|
|
823
832
|
* User data (if authentication middleware is used)
|
|
824
833
|
*/
|
|
@@ -1057,6 +1066,13 @@ export interface Route {
|
|
|
1057
1066
|
pattern?: {
|
|
1058
1067
|
exec: (url: URL) => PatternMatchResult | null
|
|
1059
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Set to `false` (via `router.withoutNativeDispatch()`) to keep this
|
|
1071
|
+
* route on the fetch-handler matcher even when the server runs with
|
|
1072
|
+
* `nativeRoutes: true` — e.g. when it must respect registration-order
|
|
1073
|
+
* precedence against an overlapping pattern.
|
|
1074
|
+
*/
|
|
1075
|
+
nativeDispatch?: boolean
|
|
1060
1076
|
}
|
|
1061
1077
|
|
|
1062
1078
|
/**
|
|
@@ -1102,6 +1118,22 @@ export interface ServerOptions<T extends WebSocketData = WebSocketData> extends
|
|
|
1102
1118
|
* These bypass the fetch handler entirely for zero-allocation serving (~15% faster).
|
|
1103
1119
|
*/
|
|
1104
1120
|
static?: Record<string, Response>
|
|
1121
|
+
/**
|
|
1122
|
+
* Opt-in: register compatible routes with `Bun.serve`'s native router
|
|
1123
|
+
* (`routes`), skipping the fetch handler's URL parsing and route
|
|
1124
|
+
* matching for them. Compatible means: standard HTTP method, no
|
|
1125
|
+
* domain scoping, no `where()`/inline constraints, no optional
|
|
1126
|
+
* params, and `{param}`s spanning whole segments. Everything else —
|
|
1127
|
+
* incompatible routes, 404/405 handling, HEAD fallback, the generic
|
|
1128
|
+
* OPTIONS preflight — still flows through the fetch handler with
|
|
1129
|
+
* identical semantics.
|
|
1130
|
+
*
|
|
1131
|
+
* Caveat: Bun matches by specificity (exact > param > wildcard).
|
|
1132
|
+
* Apps that rely on registration order between same-shape overlapping
|
|
1133
|
+
* patterns should leave this off. Routes registered after `serve()`
|
|
1134
|
+
* only join the native table after `reload()`.
|
|
1135
|
+
*/
|
|
1136
|
+
nativeRoutes?: boolean
|
|
1105
1137
|
/**
|
|
1106
1138
|
* Development mode options for Bun's dev server.
|
|
1107
1139
|
* When true, enables HMR and development features.
|
|
@@ -1273,12 +1305,33 @@ export type MiddlewareWithParams<T extends string = string> =
|
|
|
1273
1305
|
export type RouteParam<T extends string> = T extends `{${infer P}}` ? P : never
|
|
1274
1306
|
|
|
1275
1307
|
/**
|
|
1276
|
-
*
|
|
1308
|
+
* Strip an inline constraint from a parameter token:
|
|
1309
|
+
* `id:[0-9]+` → `id`, `slug` → `slug`
|
|
1310
|
+
*/
|
|
1311
|
+
export type ExtractParamName<T extends string> =
|
|
1312
|
+
T extends `${infer Name}:${string}` ? Name : T
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Extract parameters from a route path into a narrowly-typed object.
|
|
1316
|
+
*
|
|
1317
|
+
* Handles the full parameter grammar:
|
|
1318
|
+
* - `{id}` → `{ id: string }`
|
|
1319
|
+
* - `{id?}` (optional) → `{ id?: string }`
|
|
1320
|
+
* - `{id:[0-9]+}` (inline constraint) → `{ id: string }`
|
|
1321
|
+
* - trailing `*` wildcard → `{ wildcard: string }`
|
|
1322
|
+
*
|
|
1323
|
+
* @example
|
|
1324
|
+
* type P = ExtractRouteParams<'/users/{userId}/posts/{slug?}'>
|
|
1325
|
+
* // ^? { userId: string } & { slug?: string }
|
|
1277
1326
|
*/
|
|
1278
1327
|
export type ExtractRouteParams<T extends string> =
|
|
1279
1328
|
T extends `${string}{${infer Param}}${infer Rest}`
|
|
1280
|
-
?
|
|
1281
|
-
|
|
1329
|
+
? (Param extends `${infer Inner}?`
|
|
1330
|
+
? { [K in ExtractParamName<Inner>]?: string }
|
|
1331
|
+
: { [K in ExtractParamName<Param>]: string }) & ExtractRouteParams<Rest>
|
|
1332
|
+
: T extends `${string}*${string}`
|
|
1333
|
+
? { wildcard: string }
|
|
1334
|
+
: object
|
|
1282
1335
|
|
|
1283
1336
|
/**
|
|
1284
1337
|
* Route method constraints
|
|
@@ -1735,8 +1788,8 @@ export interface StreamingOptions {
|
|
|
1735
1788
|
/**
|
|
1736
1789
|
* SSE (Server-Sent Events) data structure
|
|
1737
1790
|
*/
|
|
1738
|
-
export interface SSEData {
|
|
1739
|
-
data:
|
|
1791
|
+
export interface SSEData<T = unknown> {
|
|
1792
|
+
data: T
|
|
1740
1793
|
event?: string
|
|
1741
1794
|
id?: string
|
|
1742
1795
|
retry?: number
|
|
@@ -1933,6 +1986,9 @@ export interface RequestMacroMethods {
|
|
|
1933
1986
|
bearerToken: () => string | null
|
|
1934
1987
|
basicAuth: () => { username: string, password: string } | null
|
|
1935
1988
|
|
|
1989
|
+
/** Raw unparsed request body as a string (cached). Needed for webhook signature verification (Stripe/GitHub/Slack), where a re-serialized jsonBody won't match the HMAC. */
|
|
1990
|
+
rawBody: () => Promise<string>
|
|
1991
|
+
|
|
1936
1992
|
// Headers
|
|
1937
1993
|
hasHeader: (name: string) => boolean
|
|
1938
1994
|
header: (name: string, defaultValue?: string) => string | null
|
package/src/url.ts
CHANGED
|
@@ -100,8 +100,13 @@ export function url(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// Strip any unfilled optional placeholders
|
|
104
|
-
|
|
103
|
+
// Strip any unfilled optional placeholders and tidy the slashes they
|
|
104
|
+
// leave behind (`/posts/{slug?}` → `/posts`, `/a/{v?}/b` → `/a/b`)
|
|
105
|
+
if (resolved.includes('?}')) {
|
|
106
|
+
resolved = resolved.replace(/\{[^}]+\?\}/g, '').replace(/\/{2,}/g, '/')
|
|
107
|
+
if (resolved.length > 1 && resolved.endsWith('/'))
|
|
108
|
+
resolved = resolved.slice(0, -1)
|
|
109
|
+
}
|
|
105
110
|
|
|
106
111
|
// Throw on unfilled required placeholders so the caller sees the bug.
|
|
107
112
|
const missing = resolved.match(/\{([^}?]+)\}/g)
|
package/src/utils.ts
CHANGED
|
@@ -83,6 +83,83 @@ export function createPathRegex(pattern: string): RegExp {
|
|
|
83
83
|
return new RegExp(`^${regexPattern}$`)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Per-pattern matching data. A route's pattern never changes, so the
|
|
88
|
+
* segment split, param-name extraction, and optional-param count are derived
|
|
89
|
+
* once and cached — turning `matchPath` from "regex + split on every request"
|
|
90
|
+
* into a tight array walk. Only the incoming `path` is processed per call.
|
|
91
|
+
*/
|
|
92
|
+
interface CompiledSegment {
|
|
93
|
+
isParam: boolean
|
|
94
|
+
paramName: string
|
|
95
|
+
isOptional: boolean
|
|
96
|
+
literal: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface CompiledPattern {
|
|
100
|
+
isWildcard: boolean
|
|
101
|
+
wildcardBase: string
|
|
102
|
+
segments: CompiledSegment[]
|
|
103
|
+
optionalCount: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/
|
|
107
|
+
const OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/
|
|
108
|
+
|
|
109
|
+
const compiledPatternCache = new Map<string, CompiledPattern>()
|
|
110
|
+
// Cached compiled constraint regexes; `null` marks an invalid pattern so we
|
|
111
|
+
// don't re-attempt construction on every request.
|
|
112
|
+
const constraintRegexCache = new Map<string, RegExp | null>()
|
|
113
|
+
|
|
114
|
+
function compilePattern(pattern: string): CompiledPattern {
|
|
115
|
+
const cached = compiledPatternCache.get(pattern)
|
|
116
|
+
if (cached)
|
|
117
|
+
return cached
|
|
118
|
+
|
|
119
|
+
const isWildcard = pattern.endsWith('/*')
|
|
120
|
+
const normalizedPattern = normalizePath(pattern)
|
|
121
|
+
let optionalCount = 0
|
|
122
|
+
const segments: CompiledSegment[] = normalizedPattern
|
|
123
|
+
.split('/')
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map((seg) => {
|
|
126
|
+
const isOptional = OPTIONAL_SEGMENT_RE.test(seg)
|
|
127
|
+
if (isOptional)
|
|
128
|
+
optionalCount++
|
|
129
|
+
const paramMatch = seg.match(PARAM_SEGMENT_RE)
|
|
130
|
+
return {
|
|
131
|
+
isParam: !!paramMatch,
|
|
132
|
+
paramName: paramMatch ? paramMatch[1].replace('?', '') : '',
|
|
133
|
+
isOptional,
|
|
134
|
+
literal: seg,
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const compiled: CompiledPattern = {
|
|
139
|
+
isWildcard,
|
|
140
|
+
wildcardBase: isWildcard ? pattern.slice(0, -2) : '',
|
|
141
|
+
segments,
|
|
142
|
+
optionalCount,
|
|
143
|
+
}
|
|
144
|
+
compiledPatternCache.set(pattern, compiled)
|
|
145
|
+
return compiled
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getConstraintRegex(constraint: string): RegExp | null {
|
|
149
|
+
const cached = constraintRegexCache.get(constraint)
|
|
150
|
+
if (cached !== undefined)
|
|
151
|
+
return cached
|
|
152
|
+
let regex: RegExp | null
|
|
153
|
+
try {
|
|
154
|
+
regex = new RegExp(`^${constraint}$`)
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
regex = null
|
|
158
|
+
}
|
|
159
|
+
constraintRegexCache.set(constraint, regex)
|
|
160
|
+
return regex
|
|
161
|
+
}
|
|
162
|
+
|
|
86
163
|
/**
|
|
87
164
|
* Matches a path against a pattern and extracts parameters
|
|
88
165
|
* @param pattern The path pattern (e.g., '/users/{id}')
|
|
@@ -97,71 +174,59 @@ export function matchPath(
|
|
|
97
174
|
params: Record<string, string>,
|
|
98
175
|
constraints?: Record<string, string>,
|
|
99
176
|
): boolean {
|
|
100
|
-
|
|
101
|
-
const normalizedPattern = normalizePath(pattern)
|
|
102
|
-
const normalizedPath = normalizePath(path)
|
|
177
|
+
const compiled = compilePattern(pattern)
|
|
103
178
|
|
|
104
|
-
// Special case for wildcard patterns
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
179
|
+
// Special case for wildcard patterns. Matches the original semantics:
|
|
180
|
+
// a prefix hit short-circuits to true, otherwise we fall through to the
|
|
181
|
+
// per-segment match below.
|
|
182
|
+
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
183
|
+
return true
|
|
110
184
|
}
|
|
111
185
|
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const pathSegments = normalizedPath.split('/').filter(Boolean)
|
|
186
|
+
const patternSegments = compiled.segments
|
|
187
|
+
const pathSegments = normalizePath(path).split('/').filter(Boolean)
|
|
115
188
|
|
|
116
189
|
// Quick check - if segments don't match (accounting for optional params) then no match
|
|
117
190
|
// For mandatory parameters, the pattern and path must have same number of segments
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (pathSegments.length < patternSegments.length - optionalParamCount
|
|
191
|
+
if (pathSegments.length < patternSegments.length - compiled.optionalCount
|
|
121
192
|
|| pathSegments.length > patternSegments.length) {
|
|
122
193
|
return false
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
// Match each segment
|
|
126
197
|
for (let i = 0; i < patternSegments.length; i++) {
|
|
127
|
-
const
|
|
198
|
+
const seg = patternSegments[i]
|
|
128
199
|
const pathSegment = pathSegments[i]
|
|
129
200
|
|
|
130
201
|
// If we've run out of path segments and this is not an optional parameter
|
|
131
202
|
if (pathSegment === undefined) {
|
|
132
|
-
if (
|
|
203
|
+
if (seg.isOptional) {
|
|
133
204
|
// This is an optional parameter and we have no path segment for it
|
|
134
205
|
continue
|
|
135
206
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
207
|
+
// Required parameter or static segment with no matching path segment
|
|
208
|
+
return false
|
|
140
209
|
}
|
|
141
210
|
|
|
142
|
-
|
|
143
|
-
const paramMatch = patternSegment.match(/\{([^}:]+)(?::[^}]+)?(\?)?\}/)
|
|
144
|
-
if (paramMatch) {
|
|
145
|
-
// This is a parameter segment, extract param name
|
|
146
|
-
const paramName = paramMatch[1].replace('?', '')
|
|
211
|
+
if (seg.isParam) {
|
|
147
212
|
// Store the parameter value
|
|
148
|
-
params[paramName] = pathSegment
|
|
213
|
+
params[seg.paramName] = pathSegment
|
|
149
214
|
|
|
150
215
|
// Check constraints if they exist
|
|
151
|
-
if (constraints && constraints[paramName]) {
|
|
152
|
-
|
|
153
|
-
|
|
216
|
+
if (constraints && constraints[seg.paramName]) {
|
|
217
|
+
const regex = getConstraintRegex(constraints[seg.paramName])
|
|
218
|
+
if (regex) {
|
|
154
219
|
if (!regex.test(pathSegment)) {
|
|
155
220
|
return false
|
|
156
221
|
}
|
|
157
222
|
}
|
|
158
|
-
|
|
223
|
+
else {
|
|
159
224
|
// If regex is invalid, treat as no constraint
|
|
160
|
-
console.warn(`Invalid constraint regex for parameter ${paramName}: ${constraints[paramName]}`)
|
|
225
|
+
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`)
|
|
161
226
|
}
|
|
162
227
|
}
|
|
163
228
|
}
|
|
164
|
-
else if (
|
|
229
|
+
else if (seg.literal !== pathSegment) {
|
|
165
230
|
// Static segment doesn't match
|
|
166
231
|
return false
|
|
167
232
|
}
|
|
@@ -254,7 +319,7 @@ export async function resolveViewPath(
|
|
|
254
319
|
* @param template The HTML template string
|
|
255
320
|
* @param data The data to inject into the template
|
|
256
321
|
*/
|
|
257
|
-
export function processHtmlTemplate(template: string, data: Record<string,
|
|
322
|
+
export function processHtmlTemplate(template: string, data: Record<string, unknown>): string {
|
|
258
323
|
// Process general variables {{ varName }}
|
|
259
324
|
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
260
325
|
const trimmedKey = key.trim()
|
|
@@ -274,7 +339,7 @@ export function processHtmlTemplate(template: string, data: Record<string, any>)
|
|
|
274
339
|
/**
|
|
275
340
|
* Process conditional statements in templates
|
|
276
341
|
*/
|
|
277
|
-
function processConditionals(template: string, data: Record<string,
|
|
342
|
+
function processConditionals(template: string, data: Record<string, unknown>): string {
|
|
278
343
|
// Match {{#if condition}} content {{/if}} or {{#if condition}} content {{else}} alternative {{/if}}
|
|
279
344
|
return template.replace(
|
|
280
345
|
/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
|
|
@@ -290,7 +355,7 @@ function processConditionals(template: string, data: Record<string, any>): strin
|
|
|
290
355
|
/**
|
|
291
356
|
* Process loop statements in templates
|
|
292
357
|
*/
|
|
293
|
-
function processLoops(template: string, data: Record<string,
|
|
358
|
+
function processLoops(template: string, data: Record<string, unknown>): string {
|
|
294
359
|
// Match {{#each items}} content {{/each}}
|
|
295
360
|
return template.replace(
|
|
296
361
|
/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
@@ -320,8 +385,11 @@ function processLoops(template: string, data: Record<string, any>): string {
|
|
|
320
385
|
* @param obj The object to extract value from
|
|
321
386
|
* @param path The path to the value (e.g., 'user.profile.name')
|
|
322
387
|
*/
|
|
323
|
-
function getNestedValue(obj: Record<string,
|
|
324
|
-
return path.split('.').reduce((prev, curr) => {
|
|
325
|
-
|
|
388
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
389
|
+
return path.split('.').reduce<unknown>((prev, curr) => {
|
|
390
|
+
if (prev && typeof prev === 'object' && curr in prev) {
|
|
391
|
+
return (prev as Record<string, unknown>)[curr]
|
|
392
|
+
}
|
|
393
|
+
return undefined
|
|
326
394
|
}, obj)
|
|
327
395
|
}
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
import type { EnhancedRequest, NextFunction } from '../types'
|
|
8
8
|
import { ValidationException } from '../errors/exceptions'
|
|
9
9
|
|
|
10
|
+
// Hoisted email check. Pragmatic RFC 5322 approximation: requires one `@`,
|
|
11
|
+
// a non-empty local part without surrounding/consecutive dots, and a domain
|
|
12
|
+
// with at least one label dot and a 2+ char TLD. The previous regex
|
|
13
|
+
// accepted addresses like `a@.com` and consecutive dots.
|
|
14
|
+
const EMAIL_REGEX = /^[A-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?)*\.[A-Z]{2,}$/i
|
|
15
|
+
|
|
10
16
|
export interface ValidationRule {
|
|
11
17
|
name: string
|
|
12
18
|
validate: (value: any, parameters: string[], field: string, data: Record<string, any>) => Promise<boolean> | boolean
|
|
@@ -74,10 +80,7 @@ export const BuiltInRules: Record<string, ValidationRule> = {
|
|
|
74
80
|
|
|
75
81
|
email: {
|
|
76
82
|
name: 'email',
|
|
77
|
-
validate:
|
|
78
|
-
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
|
|
79
|
-
return typeof value === 'string' && emailRegex.test(value)
|
|
80
|
-
},
|
|
83
|
+
validate: value => typeof value === 'string' && EMAIL_REGEX.test(value),
|
|
81
84
|
message: 'The :field field must be a valid email address.',
|
|
82
85
|
},
|
|
83
86
|
|
|
@@ -539,8 +539,13 @@ export class WebSocketCluster {
|
|
|
539
539
|
break
|
|
540
540
|
|
|
541
541
|
case 'stats':
|
|
542
|
-
// Handle stats updates from other workers
|
|
542
|
+
// Handle stats updates from other workers. Bounded: with worker
|
|
543
|
+
// churn the per-worker entries would otherwise accumulate forever.
|
|
543
544
|
if (message.data && message.workerId) {
|
|
545
|
+
const workerIds = Object.keys(this.stats.workerStats)
|
|
546
|
+
if (workerIds.length >= 1000 && !(message.workerId in this.stats.workerStats)) {
|
|
547
|
+
delete this.stats.workerStats[workerIds[0]]
|
|
548
|
+
}
|
|
544
549
|
this.stats.workerStats[message.workerId] = message.data
|
|
545
550
|
}
|
|
546
551
|
break
|
|
@@ -623,6 +628,8 @@ export class WebSocketCluster {
|
|
|
623
628
|
timestamp: now,
|
|
624
629
|
})
|
|
625
630
|
}, this.config.heartbeatInterval)
|
|
631
|
+
// The heartbeat must not keep the process alive on shutdown
|
|
632
|
+
this.heartbeatTimer.unref?.()
|
|
626
633
|
}
|
|
627
634
|
|
|
628
635
|
/**
|