@@ -215,6 +215,10 @@ export class Parser extends DiagnosticEmitter {
215215 dependees : Map < string , Dependee > = new Map ( ) ;
216216 /** Normalized paths whose `@rest` runtime import has already been injected. */
217217 restImportedSources : Set < string > = new Set ( ) ;
218+ /** Monotonic id handed to each `@ratelimit` route so the edge can key one
219+ * shared limiter per route. Program-wide (one Parser per program), assigned
220+ * deterministically in route declaration order. */
221+ ratelimitRouteCounter : i32 = 0 ;
218222 /** An array of parsed sources. */
219223 sources : Source [ ] ;
220224 /** Current overridden module name. */
@@ -2232,6 +2236,11 @@ export class Parser extends DiagnosticEmitter {
22322236 let s = "if(__req.method==" + methodValue . toString ( ) + "){" ;
22332237 s += "const __ctx=__toilMatch(" + JSON . stringify ( fullPath ) + ",__req);" ;
22342238 s += "if(__ctx!=null){" ;
2239+ // `@ratelimit(strategy, limit, window)`: reject (429 + Retry-After) before
2240+ // any other work, so it shields the auth/body path from a flood and rate-
2241+ // limits unauthenticated brute-force too. Keyed host-side by the unspoofable
2242+ // peer IP. Runs before the `@auth` guard.
2243+ s += this . ratelimitGuardOf ( method ) ;
22352244 // `@auth` (on the route or the controller): reject before the handler runs
22362245 // when there is no valid session. `AuthService` is an ambient global; it
22372246 // reads the current request's session cookie. The handler then calls
@@ -2290,6 +2299,54 @@ export class Parser extends DiagnosticEmitter {
22902299 return "" ;
22912300 }
22922301
2302+ /**
2303+ * The rate-limit guard to prepend to a route block when the handler carries
2304+ * `@ratelimit(strategy, limit, window)`, else `""`. `strategy` is a member of
2305+ * the `RateLimit` enum (`FixedWindow`/`SlidingWindow`/`TokenBucket`) or a bare
2306+ * integer tag; `limit` and `window` must be integer literals (mirrors
2307+ * `@cache`). Lowers to a single ambient `RateLimitService.guard(...)` call that
2308+ * returns a `429` `Response` when over the limit, or `null` to proceed. A
2309+ * malformed decorator yields `""` (no guard) rather than miscompiling.
2310+ */
2311+ private ratelimitGuardOf ( method : MethodDeclaration ) : string {
2312+ let decos = method . decorators ;
2313+ if ( decos == null ) return "" ;
2314+ for ( let i = 0 , k = decos . length ; i < k ; ++ i ) {
2315+ if ( decos [ i ] . decoratorKind != DecoratorKind . Ratelimit ) continue ;
2316+ let args = decos [ i ] . args ;
2317+ if ( args == null || args . length < 3 ) return "" ; // need (strategy, limit, window)
2318+ let tag = this . ratelimitStrategyTag ( args [ 0 ] ) ;
2319+ if ( tag . length == 0 ) return "" ; // unrecognized strategy -> no guard
2320+ let limit = args [ 1 ] ;
2321+ let window = args [ 2 ] ;
2322+ if ( ! ( limit instanceof IntegerLiteralExpression ) || ! ( window instanceof IntegerLiteralExpression ) ) {
2323+ return "" ; // non-literal limit/window -> fail safe (no guard)
2324+ }
2325+ let routeId = this . ratelimitRouteCounter ++ ;
2326+ return "{const __rl=RateLimitService.guard(" + routeId . toString ( ) + "," + tag + "," +
2327+ limit . range . toString ( ) + "," + window . range . toString ( ) + ");if(__rl!=null){return __rl;}}" ;
2328+ }
2329+ return "" ;
2330+ }
2331+
2332+ /**
2333+ * The integer strategy tag to emit for a `@ratelimit` strategy argument:
2334+ * `RateLimit.SlidingWindow` -> "1", `RateLimit.TokenBucket` -> "2", any other
2335+ * member (incl. `FixedWindow`) -> "0", an explicit integer literal passes
2336+ * through verbatim (the host clamps an unknown tag to FixedWindow). Returns
2337+ * "" when the argument is neither, so the caller emits no guard.
2338+ */
2339+ private ratelimitStrategyTag ( arg : Expression ) : string {
2340+ if ( arg instanceof IntegerLiteralExpression ) return arg . range . toString ( ) ;
2341+ if ( arg instanceof PropertyAccessExpression ) {
2342+ let p = ( < PropertyAccessExpression > arg ) . property . text ;
2343+ if ( p == "SlidingWindow" ) return "1" ;
2344+ if ( p == "TokenBucket" ) return "2" ;
2345+ return "0" ; // FixedWindow or any unknown member -> cheapest always-on default
2346+ }
2347+ return "" ;
2348+ }
2349+
22932350 /** Find an object-literal field value by name, or null. */
22942351 private objectField ( obj : ObjectLiteralExpression , name : string ) : Expression | null {
22952352 let names = obj . names ;
0 commit comments