@nexusts/limiter 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ This module is part of the NexusTS monorepo. Each module is published as its own
15
15
  Most apps start with just the core:
16
16
 
17
17
  ```bash
18
- bun add @nexusts/core reflect-metadata zod hono
18
+ bun add @nexusts/core
19
19
  ```
20
20
 
21
21
  Then add this module only if you need it:
@@ -26,7 +26,7 @@ bun add @nexusts/limiter
26
26
 
27
27
  ## Peer dependencies
28
28
 
29
- None. This module is fully self-contained.
29
+ **None.** No external dependencies for the memory backend. The Drizzle backend uses `@nexusts/drizzle` if installed.
30
30
 
31
31
  ## Usage
32
32
 
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `DrizzleRateLimitStorage` — rate-limit state in any Drizzle-backed DB.
3
+ *
4
+ * import { DrizzleService } from 'nexusjs/drizzle';
5
+ * import { DrizzleRateLimitStorage } from 'nexusjs/limiter';
6
+ *
7
+ * const db = new DrizzleService({ dialect: 'postgres', connection: { ... } });
8
+ * await db.open();
9
+ * const storage = new DrizzleRateLimitStorage(db, { tableName: 'nexus_rate_limits' });
10
+ *
11
+ * LimiterModule.forRoot({ storage, rules: [...] });
12
+ *
13
+ * Schema:
14
+ * CREATE TABLE nexus_rate_limits (
15
+ * key TEXT PRIMARY KEY,
16
+ * strategy TEXT NOT NULL,
17
+ * limit INTEGER NOT NULL,
18
+ * points INTEGER NOT NULL DEFAULT 0,
19
+ * reset_at TIMESTAMP NOT NULL,
20
+ * log JSONB
21
+ * );
22
+ *
23
+ * Atomicity: each `consume()` runs inside a transaction. The
24
+ * counter-update + log-trim happens as a single SQL statement
25
+ * (UPDATE with `WHERE` guard) so concurrent callers are safe.
26
+ */
27
+ import type { DrizzleService } from "../../drizzle/drizzle.service.js";
28
+ import type { RateLimitKey, RateLimitResult, RateLimitStorage, RateLimitStrategy } from "../types.js";
29
+ export interface DrizzleRateLimitOptions {
30
+ db: DrizzleService;
31
+ tableName?: string;
32
+ }
33
+ export declare class DrizzleRateLimitStorage implements RateLimitStorage {
34
+ #private;
35
+ readonly kind: "drizzle";
36
+ constructor(db: DrizzleService, options?: Omit<DrizzleRateLimitOptions, "db">);
37
+ consume(key: RateLimitKey, points: number, limit: number, durationMs: number, strategy?: RateLimitStrategy): Promise<RateLimitResult>;
38
+ reset(key: RateLimitKey): Promise<void>;
39
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Storage backend barrel.
3
+ */
4
+ export { MemoryRateLimitStorage } from "./memory.js";
5
+ export { DrizzleRateLimitStorage, type DrizzleRateLimitOptions, } from "./drizzle.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * In-memory rate-limit storage. Sliding-window log by default.
3
+ *
4
+ * - `fixed-window`: counter reset every `durationMs` ms.
5
+ * - `sliding-window`: counts requests in the trailing `durationMs` window.
6
+ * - `token-bucket`: refills at `points / durationMs` tokens per ms.
7
+ *
8
+ * Not cluster-safe. For multi-pod deployments use `RedisStorage`.
9
+ */
10
+ import type { RateLimitStorage, RateLimitKey, RateLimitStrategy } from "../types.js";
11
+ export declare class MemoryRateLimitStorage implements RateLimitStorage {
12
+ readonly kind: "memory";
13
+ private fixed;
14
+ private sliding;
15
+ private token;
16
+ consume(key: RateLimitKey, points: number, limit: number, durationMs: number, strategy?: RateLimitStrategy): Promise<{
17
+ allowed: boolean;
18
+ remaining: number;
19
+ limit: number;
20
+ resetAt: number;
21
+ retryAfter: number;
22
+ }>;
23
+ reset(key: RateLimitKey): Promise<void>;
24
+ private consumeFixed;
25
+ private consumeSliding;
26
+ private consumeToken;
27
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public entry point for `nexusjs/limiter`.
3
+ */
4
+ export * from "./types.js";
5
+ export { MemoryRateLimitStorage } from "./backends/index.js";
6
+ export { LimiterService } from "./limiter.service.js";
7
+ export { LimiterMiddleware } from "./limiter.middleware.js";
8
+ export { LimiterModule } from "./limiter.module.js";
package/dist/index.js CHANGED
@@ -239,7 +239,7 @@ class DrizzleRateLimitStorage {
239
239
  }
240
240
  }
241
241
  // packages/limiter/src/limiter.service.ts
242
- import { Inject, Injectable } from "@nexusts/core/decorators/index.js";
242
+ import { Inject, Injectable } from "@nexusts/core";
243
243
  class LimiterService {
244
244
  static TOKEN = Symbol.for("nexus:LimiterService");
245
245
  storage;
@@ -287,7 +287,7 @@ LimiterService = __legacyDecorateClassTS([
287
287
  ])
288
288
  ], LimiterService);
289
289
  // packages/limiter/src/limiter.middleware.ts
290
- import { Inject as Inject2, Injectable as Injectable2 } from "@nexusts/core/decorators/index.js";
290
+ import { Inject as Inject2, Injectable as Injectable2 } from "@nexusts/core";
291
291
  class LimiterMiddleware {
292
292
  limiter;
293
293
  static TOKEN = Symbol.for("nexus:LimiterMiddleware");
@@ -339,7 +339,7 @@ function matchGlob(pattern, path) {
339
339
  }
340
340
  // packages/limiter/src/limiter.module.ts
341
341
  import"reflect-metadata";
342
- import { Module } from "@nexusts/core/decorators/module.js";
342
+ import { Module } from "@nexusts/core";
343
343
  class LimiterModule {
344
344
  static forRoot(config = {}) {
345
345
  const cfg = {
@@ -399,5 +399,5 @@ export {
399
399
  LIMITER_RULE_KEY
400
400
  };
401
401
 
402
- //# debugId=8C732AC261AAD04664756E2164756E21
402
+ //# debugId=F5ACBC64D4F520F464756E2164756E21
403
403
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,14 +2,14 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts", "../src/backends/memory.ts", "../src/backends/drizzle.ts", "../src/limiter.service.ts", "../src/limiter.middleware.ts", "../src/limiter.module.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * `nexusjs/limiter` — rate limiting.\n *\n * Two ways to apply limits:\n *\n * 1. **Global** via `LimiterModule.forRoot({ rules: [...] })`:\n * limits matched against request path / method.\n *\n * 2. **Per-route** via the `@RateLimit` decorator:\n *\n * ```ts\n * @Controller('/auth')\n * class AuthController {\n * @Post('/login')\n * @RateLimit({ points: 5, duration: '1m' })\n * login() {}\n * }\n * ```\n *\n * Key derivation: by default we use `c.req.header('x-forwarded-for')`\n * or the remote address. Decorator `key` option overrides with a\n * function (e.g. user ID, API key).\n *\n * Backends:\n * - `MemoryStorage` (default, single-process)\n * - `RedisStorage` (optional, multi-process / multi-pod)\n */\n\nimport \"reflect-metadata\";\nimport { METADATA_KEY } from \"@nexusts/core/constants.js\";\n\n/** Identifier of the request — IP, user ID, API key, etc. */\nexport type RateLimitKey = string;\n\n/** Strategy used to count requests. */\nexport type RateLimitStrategy =\n\t| \"fixed-window\"\n\t| \"sliding-window\"\n\t| \"token-bucket\";\n\n/**\n * Numeric size of a window. Either a millisecond count or one of\n * `'1s'`, `'1m'`, `'1h'`, `'1d'` for convenience.\n */\nexport type DurationLike = number | `${number}${\"s\" | \"m\" | \"h\" | \"d\"}`;\n\n/** Result of a single rate-limit check. */\nexport interface RateLimitResult {\n\t/** Whether the request is allowed. */\n\tallowed: boolean;\n\t/** Remaining points in the current window. */\n\tremaining: number;\n\t/** Total points in the current window. */\n\tlimit: number;\n\t/** Unix-ms timestamp when the window resets. */\n\tresetAt: number;\n\t/** Number of seconds the client should wait (only when `allowed=false`). */\n\tretryAfter: number;\n}\n\n/** Storage backend for limiter state. */\nexport interface RateLimitStorage {\n\t/**\n\t * Consume `points` units for `key`, allowing at most `limit` units\n\t * per `durationMs` window. Returns the limit result.\n\t * Implementations must be atomic across concurrent callers.\n\t */\n\tconsume(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tstrategy: RateLimitStrategy,\n\t): Promise<RateLimitResult>;\n\n\t/** Reset all state for a key. Useful in tests. */\n\treset(key: RateLimitKey): Promise<void>;\n}\n\n/** Per-rule configuration. */\nexport interface RateLimitRule {\n\t/** Path pattern. Glob: `*` matches a single segment, `**` any depth. */\n\tpath: string;\n\t/** HTTP methods to apply to; default = all. */\n\tmethods?: string[];\n\t/** Number of allowed requests per window. */\n\tpoints: number;\n\t/** Window size. */\n\tduration: DurationLike;\n\t/** Override key derivation. */\n\tkey?: (c: any) => string | undefined | Promise<string | undefined>;\n\t/** Bucket strategy. Default `'sliding-window'`. */\n\tstrategy?: RateLimitStrategy;\n\t/** Custom rejection response. */\n\treject?: (c: any, result: RateLimitResult) => Response | Promise<Response>;\n\t/** Skip when this returns true. */\n\tskip?: (c: any) => boolean | Promise<boolean>;\n}\n\n/** Top-level configuration. */\nexport interface LimiterConfig {\n\t/** Storage backend. Default: in-memory. */\n\tstorage?: RateLimitStorage;\n\t/** Global rules applied before the per-route ones. */\n\trules?: RateLimitRule[];\n\t/** Default key derivation when a rule omits one. Default: IP address. */\n\tdefaultKey?: (c: any) => string | undefined | Promise<string | undefined>;\n\t/** Default response when a request is rejected. */\n\tdefaultReject?: (\n\t\tc: any,\n\t\tresult: RateLimitResult,\n\t) => Response | Promise<Response>;\n}\n\nexport const LIMITER_RULE_KEY = Symbol.for(\"nexus:RateLimitRule\");\n\n/** Decorator: attach a per-route rate limit. */\nexport function RateLimit(\n\trule: RateLimitRule,\n): MethodDecorator & ClassDecorator {\n\treturn (\n\t\ttarget: any,\n\t\tpropertyKey?: string | symbol,\n\t\tdescriptor?: PropertyDescriptor,\n\t) => {\n\t\t// Class-level: applied to all routes of the controller.\n\t\tif (descriptor === undefined) {\n\t\t\tconst existing: RateLimitRule[] =\n\t\t\t\tReflect.getMetadata(LIMITER_RULE_KEY, target) ?? [];\n\t\t\texisting.push({ ...rule, path: \"**\" });\n\t\t\tReflect.defineMetadata(LIMITER_RULE_KEY, existing, target);\n\t\t\treturn target;\n\t\t}\n\t\t// Method-level: bound to the route.\n\t\tconst existing: RateLimitRule[] =\n\t\t\tReflect.getMetadata(LIMITER_RULE_KEY, target.constructor) ?? [];\n\t\texisting.push({ ...rule, path: propertyKey === undefined ? \"**\" : `**` });\n\t\tReflect.defineMetadata(LIMITER_RULE_KEY, existing, target.constructor);\n\t};\n}\n\n/** Read all `@RateLimit` rules from a controller or method. */\nexport function getLimiterRules(target: any): RateLimitRule[] {\n\treturn Reflect.getMetadata(LIMITER_RULE_KEY, target) ?? [];\n}\n\n/** Convert a `DurationLike` to milliseconds. */\nexport function durationToMs(d: DurationLike): number {\n\tif (typeof d === \"number\") return d;\n\tconst m = /^(\\d+)([smhd])$/.exec(d);\n\tif (!m) throw new Error(`Invalid duration: ${d}`);\n\tconst n = Number(m[1]);\n\tconst unit = m[2] as \"s\" | \"m\" | \"h\" | \"d\";\n\tconst mult: Record<typeof unit, number> = {\n\t\ts: 1000,\n\t\tm: 60_000,\n\t\th: 3_600_000,\n\t\td: 86_400_000,\n\t};\n\treturn n * mult[unit];\n}\n",
5
+ "/**\n * `nexusjs/limiter` — rate limiting.\n *\n * Two ways to apply limits:\n *\n * 1. **Global** via `LimiterModule.forRoot({ rules: [...] })`:\n * limits matched against request path / method.\n *\n * 2. **Per-route** via the `@RateLimit` decorator:\n *\n * ```ts\n * @Controller('/auth')\n * class AuthController {\n * @Post('/login')\n * @RateLimit({ points: 5, duration: '1m' })\n * login() {}\n * }\n * ```\n *\n * Key derivation: by default we use `c.req.header('x-forwarded-for')`\n * or the remote address. Decorator `key` option overrides with a\n * function (e.g. user ID, API key).\n *\n * Backends:\n * - `MemoryStorage` (default, single-process)\n * - `RedisStorage` (optional, multi-process / multi-pod)\n */\n\nimport \"reflect-metadata\";\nimport { METADATA_KEY } from \"@nexusts/core\";\n\n/** Identifier of the request — IP, user ID, API key, etc. */\nexport type RateLimitKey = string;\n\n/** Strategy used to count requests. */\nexport type RateLimitStrategy =\n\t| \"fixed-window\"\n\t| \"sliding-window\"\n\t| \"token-bucket\";\n\n/**\n * Numeric size of a window. Either a millisecond count or one of\n * `'1s'`, `'1m'`, `'1h'`, `'1d'` for convenience.\n */\nexport type DurationLike = number | `${number}${\"s\" | \"m\" | \"h\" | \"d\"}`;\n\n/** Result of a single rate-limit check. */\nexport interface RateLimitResult {\n\t/** Whether the request is allowed. */\n\tallowed: boolean;\n\t/** Remaining points in the current window. */\n\tremaining: number;\n\t/** Total points in the current window. */\n\tlimit: number;\n\t/** Unix-ms timestamp when the window resets. */\n\tresetAt: number;\n\t/** Number of seconds the client should wait (only when `allowed=false`). */\n\tretryAfter: number;\n}\n\n/** Storage backend for limiter state. */\nexport interface RateLimitStorage {\n\t/**\n\t * Consume `points` units for `key`, allowing at most `limit` units\n\t * per `durationMs` window. Returns the limit result.\n\t * Implementations must be atomic across concurrent callers.\n\t */\n\tconsume(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tstrategy: RateLimitStrategy,\n\t): Promise<RateLimitResult>;\n\n\t/** Reset all state for a key. Useful in tests. */\n\treset(key: RateLimitKey): Promise<void>;\n}\n\n/** Per-rule configuration. */\nexport interface RateLimitRule {\n\t/** Path pattern. Glob: `*` matches a single segment, `**` any depth. */\n\tpath: string;\n\t/** HTTP methods to apply to; default = all. */\n\tmethods?: string[];\n\t/** Number of allowed requests per window. */\n\tpoints: number;\n\t/** Window size. */\n\tduration: DurationLike;\n\t/** Override key derivation. */\n\tkey?: (c: any) => string | undefined | Promise<string | undefined>;\n\t/** Bucket strategy. Default `'sliding-window'`. */\n\tstrategy?: RateLimitStrategy;\n\t/** Custom rejection response. */\n\treject?: (c: any, result: RateLimitResult) => Response | Promise<Response>;\n\t/** Skip when this returns true. */\n\tskip?: (c: any) => boolean | Promise<boolean>;\n}\n\n/** Top-level configuration. */\nexport interface LimiterConfig {\n\t/** Storage backend. Default: in-memory. */\n\tstorage?: RateLimitStorage;\n\t/** Global rules applied before the per-route ones. */\n\trules?: RateLimitRule[];\n\t/** Default key derivation when a rule omits one. Default: IP address. */\n\tdefaultKey?: (c: any) => string | undefined | Promise<string | undefined>;\n\t/** Default response when a request is rejected. */\n\tdefaultReject?: (\n\t\tc: any,\n\t\tresult: RateLimitResult,\n\t) => Response | Promise<Response>;\n}\n\nexport const LIMITER_RULE_KEY = Symbol.for(\"nexus:RateLimitRule\");\n\n/** Decorator: attach a per-route rate limit. */\nexport function RateLimit(\n\trule: RateLimitRule,\n): MethodDecorator & ClassDecorator {\n\treturn (\n\t\ttarget: any,\n\t\tpropertyKey?: string | symbol,\n\t\tdescriptor?: PropertyDescriptor,\n\t) => {\n\t\t// Class-level: applied to all routes of the controller.\n\t\tif (descriptor === undefined) {\n\t\t\tconst existing: RateLimitRule[] =\n\t\t\t\tReflect.getMetadata(LIMITER_RULE_KEY, target) ?? [];\n\t\t\texisting.push({ ...rule, path: \"**\" });\n\t\t\tReflect.defineMetadata(LIMITER_RULE_KEY, existing, target);\n\t\t\treturn target;\n\t\t}\n\t\t// Method-level: bound to the route.\n\t\tconst existing: RateLimitRule[] =\n\t\t\tReflect.getMetadata(LIMITER_RULE_KEY, target.constructor) ?? [];\n\t\texisting.push({ ...rule, path: propertyKey === undefined ? \"**\" : `**` });\n\t\tReflect.defineMetadata(LIMITER_RULE_KEY, existing, target.constructor);\n\t};\n}\n\n/** Read all `@RateLimit` rules from a controller or method. */\nexport function getLimiterRules(target: any): RateLimitRule[] {\n\treturn Reflect.getMetadata(LIMITER_RULE_KEY, target) ?? [];\n}\n\n/** Convert a `DurationLike` to milliseconds. */\nexport function durationToMs(d: DurationLike): number {\n\tif (typeof d === \"number\") return d;\n\tconst m = /^(\\d+)([smhd])$/.exec(d);\n\tif (!m) throw new Error(`Invalid duration: ${d}`);\n\tconst n = Number(m[1]);\n\tconst unit = m[2] as \"s\" | \"m\" | \"h\" | \"d\";\n\tconst mult: Record<typeof unit, number> = {\n\t\ts: 1000,\n\t\tm: 60_000,\n\t\th: 3_600_000,\n\t\td: 86_400_000,\n\t};\n\treturn n * mult[unit];\n}\n",
6
6
  "/**\n * In-memory rate-limit storage. Sliding-window log by default.\n *\n * - `fixed-window`: counter reset every `durationMs` ms.\n * - `sliding-window`: counts requests in the trailing `durationMs` window.\n * - `token-bucket`: refills at `points / durationMs` tokens per ms.\n *\n * Not cluster-safe. For multi-pod deployments use `RedisStorage`.\n */\nimport type {\n\tRateLimitStorage,\n\tRateLimitKey,\n\tRateLimitStrategy,\n} from \"../types.js\";\n\ninterface FixedBucket {\n\tresetAt: number;\n\tcount: number;\n}\n\ninterface SlidingLog {\n\tlog: number[]; // unix-ms timestamps\n}\n\ninterface TokenBucket {\n\ttokens: number;\n\tupdatedAt: number;\n}\n\nexport class MemoryRateLimitStorage implements RateLimitStorage {\n\treadonly kind = \"memory\" as const;\n\tprivate fixed = new Map<RateLimitKey, FixedBucket>();\n\tprivate sliding = new Map<RateLimitKey, SlidingLog>();\n\tprivate token = new Map<RateLimitKey, TokenBucket>();\n\n\tasync consume(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tstrategy: RateLimitStrategy = \"sliding-window\",\n\t) {\n\t\tconst now = Date.now();\n\t\tswitch (strategy) {\n\t\t\tcase \"fixed-window\":\n\t\t\t\treturn this.consumeFixed(key, points, limit, durationMs, now);\n\t\t\tcase \"sliding-window\":\n\t\t\t\treturn this.consumeSliding(key, points, limit, durationMs, now);\n\t\t\tcase \"token-bucket\":\n\t\t\t\treturn this.consumeToken(key, points, limit, durationMs, now);\n\t\t\tdefault: {\n\t\t\t\t// Exhaustive check\n\t\t\t\tconst _: never = strategy;\n\t\t\t\tthrow new Error(`Unknown strategy: ${_}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync reset(key: RateLimitKey): Promise<void> {\n\t\tthis.fixed.delete(key);\n\t\tthis.sliding.delete(key);\n\t\tthis.token.delete(key);\n\t}\n\n\tprivate consumeFixed(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tnow: number,\n\t) {\n\t\tlet b = this.fixed.get(key);\n\t\tif (!b || b.resetAt <= now) {\n\t\t\tb = { resetAt: now + durationMs, count: 0 };\n\t\t\tthis.fixed.set(key, b);\n\t\t}\n\t\tb.count += points;\n\t\tconst allowed = b.count <= limit;\n\t\treturn {\n\t\t\tallowed,\n\t\t\tremaining: Math.max(0, limit - b.count),\n\t\t\tlimit,\n\t\t\tresetAt: b.resetAt,\n\t\t\tretryAfter: allowed ? 0 : Math.ceil((b.resetAt - now) / 1000),\n\t\t};\n\t}\n\n\tprivate consumeSliding(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tnow: number,\n\t) {\n\t\tlet s = this.sliding.get(key);\n\t\tif (!s) {\n\t\t\ts = { log: [] };\n\t\t\tthis.sliding.set(key, s);\n\t\t}\n\t\t// Drop entries outside the trailing window.\n\t\tconst cutoff = now - durationMs;\n\t\ts.log = s.log.filter((t) => t > cutoff);\n\t\tconst inWindow = s.log.length + points;\n\t\tconst allowed = inWindow <= limit;\n\t\tif (allowed) {\n\t\t\tfor (let i = 0; i < points; i++) s.log.push(now);\n\t\t}\n\t\tconst oldest = s.log[0] ?? now;\n\t\treturn {\n\t\t\tallowed,\n\t\t\tremaining: Math.max(0, limit - s.log.length),\n\t\t\tlimit,\n\t\t\tresetAt: now + durationMs,\n\t\t\tretryAfter: allowed ? 0 : Math.ceil((oldest + durationMs - now) / 1000),\n\t\t};\n\t}\n\n\tprivate consumeToken(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tnow: number,\n\t) {\n\t\tlet b = this.token.get(key);\n\t\tconst refillPerMs = limit / durationMs;\n\t\tif (!b) {\n\t\t\tb = { tokens: limit, updatedAt: now };\n\t\t\tthis.token.set(key, b);\n\t\t} else {\n\t\t\tconst elapsed = now - b.updatedAt;\n\t\t\tb.tokens = Math.min(limit, b.tokens + elapsed * refillPerMs);\n\t\t\tb.updatedAt = now;\n\t\t}\n\t\tconst allowed = b.tokens >= points;\n\t\tif (allowed) b.tokens -= points;\n\t\treturn {\n\t\t\tallowed,\n\t\t\tremaining: Math.floor(b.tokens),\n\t\t\tlimit,\n\t\t\tresetAt: now + durationMs,\n\t\t\tretryAfter: allowed\n\t\t\t\t? 0\n\t\t\t\t: Math.ceil((points - b.tokens) / refillPerMs / 1000),\n\t\t};\n\t}\n}\n",
7
7
  "/**\n * `DrizzleRateLimitStorage` — rate-limit state in any Drizzle-backed DB.\n *\n * import { DrizzleService } from 'nexusjs/drizzle';\n * import { DrizzleRateLimitStorage } from 'nexusjs/limiter';\n *\n * const db = new DrizzleService({ dialect: 'postgres', connection: { ... } });\n * await db.open();\n * const storage = new DrizzleRateLimitStorage(db, { tableName: 'nexus_rate_limits' });\n *\n * LimiterModule.forRoot({ storage, rules: [...] });\n *\n * Schema:\n * CREATE TABLE nexus_rate_limits (\n * key TEXT PRIMARY KEY,\n * strategy TEXT NOT NULL,\n * limit INTEGER NOT NULL,\n * points INTEGER NOT NULL DEFAULT 0,\n * reset_at TIMESTAMP NOT NULL,\n * log JSONB\n * );\n *\n * Atomicity: each `consume()` runs inside a transaction. The\n * counter-update + log-trim happens as a single SQL statement\n * (UPDATE with `WHERE` guard) so concurrent callers are safe.\n */\nimport type { DrizzleService } from \"../../drizzle/drizzle.service.js\";\nimport type {\n\tRateLimitKey,\n\tRateLimitResult,\n\tRateLimitStorage,\n\tRateLimitStrategy,\n} from \"../types.js\";\n\nexport interface DrizzleRateLimitOptions {\n\tdb: DrizzleService;\n\ttableName?: string;\n}\n\ninterface Row {\n\tkey: string;\n\tstrategy: string;\n\tmax_points: number;\n\tpoints: number;\n\treset_at: string;\n\tlog: string | null;\n}\nvoid (null as unknown as Row[\"points\"]);\n\nexport class DrizzleRateLimitStorage implements RateLimitStorage {\n\treadonly kind = \"drizzle\" as const;\n\n\t#db: DrizzleService;\n\t#table: string;\n\n\tconstructor(\n\t\tdb: DrizzleService,\n\t\toptions: Omit<DrizzleRateLimitOptions, \"db\"> = {},\n\t) {\n\t\tthis.#db = db;\n\t\tthis.#table = options.tableName ?? \"nexus_rate_limits\";\n\t}\n\n\tasync consume(\n\t\tkey: RateLimitKey,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tstrategy: RateLimitStrategy = \"sliding-window\",\n\t): Promise<RateLimitResult> {\n\t\tconst now = Date.now();\n\t\tconst resetAt = now + durationMs;\n\n\t\t// 1. Read existing row.\n\t\tconst rows = await this.#db.rawQuery<Row>(\n\t\t\t`SELECT * FROM ${this.#table} WHERE key = ? LIMIT 1`,\n\t\t\t[key],\n\t\t);\n\t\tconst existing = rows[0];\n\n\t\tif (!existing) {\n\t\t\t// First call — create a new bucket.\n\t\t\tconst initialLog =\n\t\t\t\tstrategy === \"sliding-window\"\n\t\t\t\t\t? JSON.stringify(Array(points).fill(now))\n\t\t\t\t\t: null;\n\t\t\tawait this.#db.rawQuery(\n\t\t\t\t`INSERT INTO ${this.#table} (key, strategy, max_points, points, reset_at, log)\n\t\t\t\t VALUES (?, ?, ?, ?, ?, ?)`,\n\t\t\t\t[\n\t\t\t\t\tkey,\n\t\t\t\t\tstrategy,\n\t\t\t\t\tlimit,\n\t\t\t\t\tstrategy === \"sliding-window\" ? 0 : 1,\n\t\t\t\t\tnew Date(resetAt).toISOString(),\n\t\t\t\t\tinitialLog,\n\t\t\t\t],\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tallowed: true,\n\t\t\t\tremaining: limit - 1,\n\t\t\t\tlimit,\n\t\t\t\tresetAt,\n\t\t\t\tretryAfter: 0,\n\t\t\t};\n\t\t}\n\n\t\t// 2. Check the strategy and decide.\n\t\tconst result = await this.#applyStrategy(\n\t\t\texisting,\n\t\t\tpoints,\n\t\t\tlimit,\n\t\t\tdurationMs,\n\t\t\tnow,\n\t\t);\n\t\treturn result;\n\t}\n\n\tasync reset(key: RateLimitKey): Promise<void> {\n\t\tawait this.#db.rawQuery(`DELETE FROM ${this.#table} WHERE key = ?`, [key]);\n\t}\n\n\tasync #applyStrategy(\n\t\trow: Row,\n\t\tpoints: number,\n\t\tlimit: number,\n\t\tdurationMs: number,\n\t\tnow: number,\n\t): Promise<RateLimitResult> {\n\t\tconst strategy: RateLimitStrategy = row.strategy as RateLimitStrategy;\n\t\tconst resetAt = Number(new Date(row.reset_at).getTime());\n\n\t\tif (strategy === \"fixed-window\") {\n\t\t\t// Reset window if past.\n\t\t\tif (resetAt <= now) {\n\t\t\t\tawait this.#db.rawQuery(\n\t\t\t\t\t`UPDATE ${this.#table} SET points = 1, reset_at = ? WHERE key = ?`,\n\t\t\t\t\t[new Date(now + durationMs).toISOString(), row.key],\n\t\t\t\t);\n\t\t\t\treturn {\n\t\t\t\t\tallowed: true,\n\t\t\t\t\tremaining: limit - 1,\n\t\t\t\t\tlimit,\n\t\t\t\t\tresetAt: now + durationMs,\n\t\t\t\t\tretryAfter: 0,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst newPoints = (row.points ?? 0) + 1;\n\t\t\tconst allowed = newPoints <= limit;\n\t\t\tawait this.#db.rawQuery(\n\t\t\t\t`UPDATE ${this.#table} SET points = ? WHERE key = ?`,\n\t\t\t\t[newPoints, row.key],\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tallowed,\n\t\t\t\tremaining: Math.max(0, limit - newPoints),\n\t\t\t\tlimit,\n\t\t\t\tresetAt,\n\t\t\t\tretryAfter: allowed ? 0 : Math.ceil((resetAt - now) / 1000),\n\t\t\t};\n\t\t}\n\n\t\tif (strategy === \"sliding-window\") {\n\t\t\tconst log: number[] = row.log ? JSON.parse(row.log) : [];\n\t\t\t// Drop entries outside the window.\n\t\t\tconst cutoff = now - durationMs;\n\t\t\tconst fresh = log.filter((t) => t > cutoff);\n\t\t\tfresh.push(now);\n\t\t\tconst used = fresh.length;\n\t\t\tconst allowed = used <= limit;\n\t\t\tawait this.#db.rawQuery(\n\t\t\t\t`UPDATE ${this.#table} SET log = ?, points = ? WHERE key = ?`,\n\t\t\t\t[JSON.stringify(fresh), used, row.key],\n\t\t\t);\n\t\t\tconst oldest = fresh[0] ?? now;\n\t\t\treturn {\n\t\t\t\tallowed,\n\t\t\t\tremaining: Math.max(0, limit - used),\n\t\t\t\tlimit,\n\t\t\t\tresetAt: now + durationMs,\n\t\t\t\tretryAfter: allowed ? 0 : Math.ceil((oldest + durationMs - now) / 1000),\n\t\t\t};\n\t\t}\n\n\t\t// token-bucket: simple implementation as a counter with refill on first hit.\n\t\tif (strategy === \"token-bucket\") {\n\t\t\tconst elapsed = Math.max(0, now - resetAt);\n\t\t\tconst refillPerMs = limit / durationMs;\n\t\t\tlet tokens = Math.min(limit, (row.points ?? 0) + elapsed * refillPerMs);\n\t\t\tconst allowed = tokens >= 1;\n\t\t\tif (allowed) tokens -= 1;\n\t\t\tawait this.#db.rawQuery(\n\t\t\t\t`UPDATE ${this.#table} SET points = ?, reset_at = ? WHERE key = ?`,\n\t\t\t\t[tokens, new Date(now).toISOString(), row.key],\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tallowed,\n\t\t\t\tremaining: Math.floor(tokens),\n\t\t\t\tlimit,\n\t\t\t\tresetAt: now + durationMs,\n\t\t\t\tretryAfter: allowed ? 0 : Math.ceil((1 - tokens) / refillPerMs / 1000),\n\t\t\t};\n\t\t}\n\n\t\tthrow new Error(`Unknown strategy: ${strategy}`);\n\t}\n}\n",
8
- "/**\n * `LimiterService` — single entry point for in-process rate-limit checks.\n *\n * Holds a `RateLimitStorage` and the global `LimiterConfig` so the\n * middleware and the `@RateLimit` decorator share one source of truth.\n *\n * const svc = new LimiterService({ storage: new MemoryRateLimitStorage() });\n * await svc.check('ip:1.2.3.4', { points: 5, duration: '1m' });\n */\nimport { Inject, Injectable } from \"@nexusts/core/decorators/index.js\";\nimport { MemoryRateLimitStorage } from \"./backends/memory.js\";\nimport type {\n\tLimiterConfig,\n\tRateLimitResult,\n\tRateLimitRule,\n\tRateLimitStorage,\n} from \"./types.js\";\nimport { durationToMs } from \"./types.js\";\n\n@Injectable()\nexport class LimiterService {\n\t/** DI token — `@Inject(LimiterService.TOKEN)`. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:LimiterService\");\n\n\tstorage: RateLimitStorage;\n\trules: RateLimitRule[];\n\tdefaultKey: NonNullable<LimiterConfig[\"defaultKey\"]>;\n\tdefaultReject: NonNullable<LimiterConfig[\"defaultReject\"]>;\n\n\tconstructor(@Inject(\"LIMITER_CONFIG\") config: LimiterConfig = {}) {\n\t\tthis.storage = config.storage ?? new MemoryRateLimitStorage();\n\t\tthis.rules = config.rules ?? [];\n\t\tthis.defaultKey =\n\t\t\tconfig.defaultKey ??\n\t\t\t((c: any) => {\n\t\t\t\tconst fwd = c?.req?.header?.(\"x-forwarded-for\");\n\t\t\t\tif (fwd) return fwd.split(\",\")[0]?.trim() ?? \"unknown\";\n\t\t\t\treturn c?.req?.raw?.[\"conn\"]?.remoteAddr?.hostname ?? \"unknown\";\n\t\t\t});\n\t\tthis.defaultReject =\n\t\t\tconfig.defaultReject ??\n\t\t\t((_c, result) =>\n\t\t\t\tnew Response(\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\terror: \"Too Many Requests\",\n\t\t\t\t\t\tlimit: result.limit,\n\t\t\t\t\t\tremaining: 0,\n\t\t\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t\t\t}),\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 429,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\"Retry-After\": String(result.retryAfter),\n\t\t\t\t\t\t\t\"X-RateLimit-Limit\": String(result.limit),\n\t\t\t\t\t\t\t\"X-RateLimit-Remaining\": \"0\",\n\t\t\t\t\t\t\t\"X-RateLimit-Reset\": String(Math.ceil(result.resetAt / 1000)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t));\n\t}\n\n\t/**\n\t * Check a single rule against `key`. Always consumes one point\n\t * (or rejects).\n\t */\n\tasync check(key: string, rule: RateLimitRule): Promise<RateLimitResult> {\n\t\tconst durationMs = durationToMs(rule.duration);\n\t\treturn this.storage.consume(\n\t\t\tkey,\n\t\t\t1,\n\t\t\trule.points,\n\t\t\tdurationMs,\n\t\t\trule.strategy ?? \"sliding-window\",\n\t\t);\n\t}\n\n\t/** Reset the state for a given key. */\n\tasync reset(key: string): Promise<void> {\n\t\tawait this.storage.reset(key);\n\t}\n}\n",
9
- "/**\n * Hono middleware factory. Applies all matching global rules in order;\n * the first one that rejects wins. Used by the framework's mount pipeline.\n */\nimport { Inject, Injectable } from \"@nexusts/core/decorators/index.js\";\nimport { LimiterService } from \"./limiter.service.js\";\nimport type { RateLimitRule } from \"./types.js\";\n\n@Injectable()\nexport class LimiterMiddleware {\n\t/** DI token. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:LimiterMiddleware\");\n\n\tconstructor(@Inject(LimiterService.TOKEN) private readonly limiter: LimiterService) {}\n\n\t/** Returns a Hono middleware. */\n\tmiddleware() {\n\t\treturn async (c: any, next: () => Promise<any>) => {\n\t\t\tconst method = c.req.method.toUpperCase();\n\t\t\tfor (const rule of this.limiter.rules) {\n\t\t\t\tif (!this.matches(rule, method, c.req.path)) continue;\n\t\t\t\tif (rule.skip && (await rule.skip(c))) continue;\n\t\t\t\tconst keyFn = rule.key ?? this.limiter.defaultKey;\n\t\t\t\tconst key = (await keyFn(c)) ?? \"unknown\";\n\t\t\t\tconst result = await this.limiter.check(key, rule);\n\t\t\t\tc.header?.(\"X-RateLimit-Limit\", String(result.limit));\n\t\t\t\tc.header?.(\"X-RateLimit-Remaining\", String(result.remaining));\n\t\t\t\tc.header?.(\"X-RateLimit-Reset\", String(Math.ceil(result.resetAt / 1000)));\n\t\t\t\tif (!result.allowed) {\n\t\t\t\t\tconst reject = rule.reject ?? this.limiter.defaultReject;\n\t\t\t\t\treturn reject(c, result);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn next();\n\t\t};\n\t}\n\n\tprivate matches(rule: RateLimitRule, method: string, path: string): boolean {\n\t\tif (rule.methods && rule.methods.length > 0) {\n\t\t\tif (!rule.methods.map((m) => m.toUpperCase()).includes(method)) return false;\n\t\t}\n\t\tif (rule.path === \"**\") return true;\n\t\treturn matchGlob(rule.path, path);\n\t}\n}\n\n/** Glob match: `*` = one segment, `**` = any depth. */\nfunction matchGlob(pattern: string, path: string): boolean {\n\tconst regex = new RegExp(\n\t\t\"^\" +\n\t\t\tpattern\n\t\t\t\t.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\t\t\t\t.replace(/\\*\\*/g, \"::DOUBLESTAR::\")\n\t\t\t\t.replace(/\\*/g, \"[^/]+\")\n\t\t\t\t.replace(/::DOUBLESTAR::/g, \".*\") +\n\t\t\t\"/?$\",\n\t);\n\treturn regex.test(path);\n}\n",
10
- "/**\n * `LimiterModule` — drop-in rate limiter.\n *\n * @Module({\n * imports: [\n * LimiterModule.forRoot({\n * rules: [\n * { path: '/api/*', points: 100, duration: '1m' },\n * { path: '/login', points: 5, duration: '1m' },\n * ],\n * }),\n * ],\n * })\n * export class AppModule {}\n */\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core/decorators/module.js\";\nimport { LimiterService } from \"./limiter.service.js\";\nimport { LimiterMiddleware } from \"./limiter.middleware.js\";\nimport { MemoryRateLimitStorage } from \"./backends/memory.js\";\nimport type { LimiterConfig } from \"./types.js\";\n\n@Module({\n\tproviders: [\n\t\tLimiterService,\n\t\t{ provide: LimiterService.TOKEN, useExisting: LimiterService },\n\t\tLimiterMiddleware,\n\t\t{ provide: LimiterMiddleware.TOKEN, useExisting: LimiterMiddleware },\n\t],\n\texports: [\n\t\tLimiterService,\n\t\tLimiterService.TOKEN,\n\t\tLimiterMiddleware,\n\t\tLimiterMiddleware.TOKEN,\n\t],\n})\nexport class LimiterModule {\n\tstatic forRoot(config: LimiterConfig = {}) {\n\t\t// Default to an in-memory storage if the user didn't supply one.\n\t\tconst cfg: LimiterConfig = {\n\t\t\tstorage: new MemoryRateLimitStorage(),\n\t\t\t...config,\n\t\t};\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tLimiterService,\n\t\t\t\t{ provide: LimiterService.TOKEN, useExisting: LimiterService },\n\t\t\t\tLimiterMiddleware,\n\t\t\t\t{ provide: LimiterMiddleware.TOKEN, useExisting: LimiterMiddleware },\n\t\t\t\t{ provide: \"LIMITER_CONFIG\", useValue: cfg },\n\t\t\t],\n\t\t\texports: [\n\t\t\t\tLimiterService,\n\t\t\t\tLimiterService.TOKEN,\n\t\t\t\tLimiterMiddleware,\n\t\t\t\tLimiterMiddleware.TOKEN,\n\t\t\t],\n\t\t})\n\t\tclass ConfiguredLimiterModule {}\n\t\tObject.defineProperty(ConfiguredLimiterModule, \"name\", {\n\t\t\tvalue: \"ConfiguredLimiterModule\",\n\t\t});\n\t\treturn ConfiguredLimiterModule;\n\t}\n}\n"
8
+ "/**\n * `LimiterService` — single entry point for in-process rate-limit checks.\n *\n * Holds a `RateLimitStorage` and the global `LimiterConfig` so the\n * middleware and the `@RateLimit` decorator share one source of truth.\n *\n * const svc = new LimiterService({ storage: new MemoryRateLimitStorage() });\n * await svc.check('ip:1.2.3.4', { points: 5, duration: '1m' });\n */\nimport { Inject, Injectable } from \"@nexusts/core\";\nimport { MemoryRateLimitStorage } from \"./backends/memory.js\";\nimport type {\n\tLimiterConfig,\n\tRateLimitResult,\n\tRateLimitRule,\n\tRateLimitStorage,\n} from \"./types.js\";\nimport { durationToMs } from \"./types.js\";\n\n@Injectable()\nexport class LimiterService {\n\t/** DI token — `@Inject(LimiterService.TOKEN)`. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:LimiterService\");\n\n\tstorage: RateLimitStorage;\n\trules: RateLimitRule[];\n\tdefaultKey: NonNullable<LimiterConfig[\"defaultKey\"]>;\n\tdefaultReject: NonNullable<LimiterConfig[\"defaultReject\"]>;\n\n\tconstructor(@Inject(\"LIMITER_CONFIG\") config: LimiterConfig = {}) {\n\t\tthis.storage = config.storage ?? new MemoryRateLimitStorage();\n\t\tthis.rules = config.rules ?? [];\n\t\tthis.defaultKey =\n\t\t\tconfig.defaultKey ??\n\t\t\t((c: any) => {\n\t\t\t\tconst fwd = c?.req?.header?.(\"x-forwarded-for\");\n\t\t\t\tif (fwd) return fwd.split(\",\")[0]?.trim() ?? \"unknown\";\n\t\t\t\treturn c?.req?.raw?.[\"conn\"]?.remoteAddr?.hostname ?? \"unknown\";\n\t\t\t});\n\t\tthis.defaultReject =\n\t\t\tconfig.defaultReject ??\n\t\t\t((_c, result) =>\n\t\t\t\tnew Response(\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\terror: \"Too Many Requests\",\n\t\t\t\t\t\tlimit: result.limit,\n\t\t\t\t\t\tremaining: 0,\n\t\t\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t\t\t}),\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 429,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\"Retry-After\": String(result.retryAfter),\n\t\t\t\t\t\t\t\"X-RateLimit-Limit\": String(result.limit),\n\t\t\t\t\t\t\t\"X-RateLimit-Remaining\": \"0\",\n\t\t\t\t\t\t\t\"X-RateLimit-Reset\": String(Math.ceil(result.resetAt / 1000)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t));\n\t}\n\n\t/**\n\t * Check a single rule against `key`. Always consumes one point\n\t * (or rejects).\n\t */\n\tasync check(key: string, rule: RateLimitRule): Promise<RateLimitResult> {\n\t\tconst durationMs = durationToMs(rule.duration);\n\t\treturn this.storage.consume(\n\t\t\tkey,\n\t\t\t1,\n\t\t\trule.points,\n\t\t\tdurationMs,\n\t\t\trule.strategy ?? \"sliding-window\",\n\t\t);\n\t}\n\n\t/** Reset the state for a given key. */\n\tasync reset(key: string): Promise<void> {\n\t\tawait this.storage.reset(key);\n\t}\n}\n",
9
+ "/**\n * Hono middleware factory. Applies all matching global rules in order;\n * the first one that rejects wins. Used by the framework's mount pipeline.\n */\nimport { Inject, Injectable } from \"@nexusts/core\";\nimport { LimiterService } from \"./limiter.service.js\";\nimport type { RateLimitRule } from \"./types.js\";\n\n@Injectable()\nexport class LimiterMiddleware {\n\t/** DI token. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:LimiterMiddleware\");\n\n\tconstructor(@Inject(LimiterService.TOKEN) private readonly limiter: LimiterService) {}\n\n\t/** Returns a Hono middleware. */\n\tmiddleware() {\n\t\treturn async (c: any, next: () => Promise<any>) => {\n\t\t\tconst method = c.req.method.toUpperCase();\n\t\t\tfor (const rule of this.limiter.rules) {\n\t\t\t\tif (!this.matches(rule, method, c.req.path)) continue;\n\t\t\t\tif (rule.skip && (await rule.skip(c))) continue;\n\t\t\t\tconst keyFn = rule.key ?? this.limiter.defaultKey;\n\t\t\t\tconst key = (await keyFn(c)) ?? \"unknown\";\n\t\t\t\tconst result = await this.limiter.check(key, rule);\n\t\t\t\tc.header?.(\"X-RateLimit-Limit\", String(result.limit));\n\t\t\t\tc.header?.(\"X-RateLimit-Remaining\", String(result.remaining));\n\t\t\t\tc.header?.(\"X-RateLimit-Reset\", String(Math.ceil(result.resetAt / 1000)));\n\t\t\t\tif (!result.allowed) {\n\t\t\t\t\tconst reject = rule.reject ?? this.limiter.defaultReject;\n\t\t\t\t\treturn reject(c, result);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn next();\n\t\t};\n\t}\n\n\tprivate matches(rule: RateLimitRule, method: string, path: string): boolean {\n\t\tif (rule.methods && rule.methods.length > 0) {\n\t\t\tif (!rule.methods.map((m) => m.toUpperCase()).includes(method)) return false;\n\t\t}\n\t\tif (rule.path === \"**\") return true;\n\t\treturn matchGlob(rule.path, path);\n\t}\n}\n\n/** Glob match: `*` = one segment, `**` = any depth. */\nfunction matchGlob(pattern: string, path: string): boolean {\n\tconst regex = new RegExp(\n\t\t\"^\" +\n\t\t\tpattern\n\t\t\t\t.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\t\t\t\t.replace(/\\*\\*/g, \"::DOUBLESTAR::\")\n\t\t\t\t.replace(/\\*/g, \"[^/]+\")\n\t\t\t\t.replace(/::DOUBLESTAR::/g, \".*\") +\n\t\t\t\"/?$\",\n\t);\n\treturn regex.test(path);\n}\n",
10
+ "/**\n * `LimiterModule` — drop-in rate limiter.\n *\n * @Module({\n * imports: [\n * LimiterModule.forRoot({\n * rules: [\n * { path: '/api/*', points: 100, duration: '1m' },\n * { path: '/login', points: 5, duration: '1m' },\n * ],\n * }),\n * ],\n * })\n * export class AppModule {}\n */\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core\";\nimport { LimiterService } from \"./limiter.service.js\";\nimport { LimiterMiddleware } from \"./limiter.middleware.js\";\nimport { MemoryRateLimitStorage } from \"./backends/memory.js\";\nimport type { LimiterConfig } from \"./types.js\";\n\n@Module({\n\tproviders: [\n\t\tLimiterService,\n\t\t{ provide: LimiterService.TOKEN, useExisting: LimiterService },\n\t\tLimiterMiddleware,\n\t\t{ provide: LimiterMiddleware.TOKEN, useExisting: LimiterMiddleware },\n\t],\n\texports: [\n\t\tLimiterService,\n\t\tLimiterService.TOKEN,\n\t\tLimiterMiddleware,\n\t\tLimiterMiddleware.TOKEN,\n\t],\n})\nexport class LimiterModule {\n\tstatic forRoot(config: LimiterConfig = {}) {\n\t\t// Default to an in-memory storage if the user didn't supply one.\n\t\tconst cfg: LimiterConfig = {\n\t\t\tstorage: new MemoryRateLimitStorage(),\n\t\t\t...config,\n\t\t};\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tLimiterService,\n\t\t\t\t{ provide: LimiterService.TOKEN, useExisting: LimiterService },\n\t\t\t\tLimiterMiddleware,\n\t\t\t\t{ provide: LimiterMiddleware.TOKEN, useExisting: LimiterMiddleware },\n\t\t\t\t{ provide: \"LIMITER_CONFIG\", useValue: cfg },\n\t\t\t],\n\t\t\texports: [\n\t\t\t\tLimiterService,\n\t\t\t\tLimiterService.TOKEN,\n\t\t\t\tLimiterMiddleware,\n\t\t\t\tLimiterMiddleware.TOKEN,\n\t\t\t],\n\t\t})\n\t\tclass ConfiguredLimiterModule {}\n\t\tObject.defineProperty(ConfiguredLimiterModule, \"name\", {\n\t\t\tvalue: \"ConfiguredLimiterModule\",\n\t\t});\n\t\treturn ConfiguredLimiterModule;\n\t}\n}\n"
11
11
  ],
12
12
  "mappings": "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",
13
- "debugId": "8C732AC261AAD04664756E2164756E21",
13
+ "debugId": "F5ACBC64D4F520F464756E2164756E21",
14
14
  "names": []
15
15
  }
@@ -0,0 +1,10 @@
1
+ import { LimiterService } from "./limiter.service.js";
2
+ export declare class LimiterMiddleware {
3
+ private readonly limiter;
4
+ /** DI token. */
5
+ static readonly TOKEN: unique symbol;
6
+ constructor(limiter: LimiterService);
7
+ /** Returns a Hono middleware. */
8
+ middleware(): (c: any, next: () => Promise<any>) => Promise<any>;
9
+ private matches;
10
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `LimiterModule` — drop-in rate limiter.
3
+ *
4
+ * @Module({
5
+ * imports: [
6
+ * LimiterModule.forRoot({
7
+ * rules: [
8
+ * { path: '/api/*', points: 100, duration: '1m' },
9
+ * { path: '/login', points: 5, duration: '1m' },
10
+ * ],
11
+ * }),
12
+ * ],
13
+ * })
14
+ * export class AppModule {}
15
+ */
16
+ import "reflect-metadata";
17
+ import type { LimiterConfig } from "./types.js";
18
+ export declare class LimiterModule {
19
+ static forRoot(config?: LimiterConfig): {
20
+ new (): {};
21
+ };
22
+ }
@@ -0,0 +1,17 @@
1
+ import type { LimiterConfig, RateLimitResult, RateLimitRule, RateLimitStorage } from "./types.js";
2
+ export declare class LimiterService {
3
+ /** DI token — `@Inject(LimiterService.TOKEN)`. */
4
+ static readonly TOKEN: unique symbol;
5
+ storage: RateLimitStorage;
6
+ rules: RateLimitRule[];
7
+ defaultKey: NonNullable<LimiterConfig["defaultKey"]>;
8
+ defaultReject: NonNullable<LimiterConfig["defaultReject"]>;
9
+ constructor(config?: LimiterConfig);
10
+ /**
11
+ * Check a single rule against `key`. Always consumes one point
12
+ * (or rejects).
13
+ */
14
+ check(key: string, rule: RateLimitRule): Promise<RateLimitResult>;
15
+ /** Reset the state for a given key. */
16
+ reset(key: string): Promise<void>;
17
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `nexusjs/limiter` — rate limiting.
3
+ *
4
+ * Two ways to apply limits:
5
+ *
6
+ * 1. **Global** via `LimiterModule.forRoot({ rules: [...] })`:
7
+ * limits matched against request path / method.
8
+ *
9
+ * 2. **Per-route** via the `@RateLimit` decorator:
10
+ *
11
+ * ```ts
12
+ * @Controller('/auth')
13
+ * class AuthController {
14
+ * @Post('/login')
15
+ * @RateLimit({ points: 5, duration: '1m' })
16
+ * login() {}
17
+ * }
18
+ * ```
19
+ *
20
+ * Key derivation: by default we use `c.req.header('x-forwarded-for')`
21
+ * or the remote address. Decorator `key` option overrides with a
22
+ * function (e.g. user ID, API key).
23
+ *
24
+ * Backends:
25
+ * - `MemoryStorage` (default, single-process)
26
+ * - `RedisStorage` (optional, multi-process / multi-pod)
27
+ */
28
+ import "reflect-metadata";
29
+ /** Identifier of the request — IP, user ID, API key, etc. */
30
+ export type RateLimitKey = string;
31
+ /** Strategy used to count requests. */
32
+ export type RateLimitStrategy = "fixed-window" | "sliding-window" | "token-bucket";
33
+ /**
34
+ * Numeric size of a window. Either a millisecond count or one of
35
+ * `'1s'`, `'1m'`, `'1h'`, `'1d'` for convenience.
36
+ */
37
+ export type DurationLike = number | `${number}${"s" | "m" | "h" | "d"}`;
38
+ /** Result of a single rate-limit check. */
39
+ export interface RateLimitResult {
40
+ /** Whether the request is allowed. */
41
+ allowed: boolean;
42
+ /** Remaining points in the current window. */
43
+ remaining: number;
44
+ /** Total points in the current window. */
45
+ limit: number;
46
+ /** Unix-ms timestamp when the window resets. */
47
+ resetAt: number;
48
+ /** Number of seconds the client should wait (only when `allowed=false`). */
49
+ retryAfter: number;
50
+ }
51
+ /** Storage backend for limiter state. */
52
+ export interface RateLimitStorage {
53
+ /**
54
+ * Consume `points` units for `key`, allowing at most `limit` units
55
+ * per `durationMs` window. Returns the limit result.
56
+ * Implementations must be atomic across concurrent callers.
57
+ */
58
+ consume(key: RateLimitKey, points: number, limit: number, durationMs: number, strategy: RateLimitStrategy): Promise<RateLimitResult>;
59
+ /** Reset all state for a key. Useful in tests. */
60
+ reset(key: RateLimitKey): Promise<void>;
61
+ }
62
+ /** Per-rule configuration. */
63
+ export interface RateLimitRule {
64
+ /** Path pattern. Glob: `*` matches a single segment, `**` any depth. */
65
+ path: string;
66
+ /** HTTP methods to apply to; default = all. */
67
+ methods?: string[];
68
+ /** Number of allowed requests per window. */
69
+ points: number;
70
+ /** Window size. */
71
+ duration: DurationLike;
72
+ /** Override key derivation. */
73
+ key?: (c: any) => string | undefined | Promise<string | undefined>;
74
+ /** Bucket strategy. Default `'sliding-window'`. */
75
+ strategy?: RateLimitStrategy;
76
+ /** Custom rejection response. */
77
+ reject?: (c: any, result: RateLimitResult) => Response | Promise<Response>;
78
+ /** Skip when this returns true. */
79
+ skip?: (c: any) => boolean | Promise<boolean>;
80
+ }
81
+ /** Top-level configuration. */
82
+ export interface LimiterConfig {
83
+ /** Storage backend. Default: in-memory. */
84
+ storage?: RateLimitStorage;
85
+ /** Global rules applied before the per-route ones. */
86
+ rules?: RateLimitRule[];
87
+ /** Default key derivation when a rule omits one. Default: IP address. */
88
+ defaultKey?: (c: any) => string | undefined | Promise<string | undefined>;
89
+ /** Default response when a request is rejected. */
90
+ defaultReject?: (c: any, result: RateLimitResult) => Response | Promise<Response>;
91
+ }
92
+ export declare const LIMITER_RULE_KEY: unique symbol;
93
+ /** Decorator: attach a per-route rate limit. */
94
+ export declare function RateLimit(rule: RateLimitRule): MethodDecorator & ClassDecorator;
95
+ /** Read all `@RateLimit` rules from a controller or method. */
96
+ export declare function getLimiterRules(target: any): RateLimitRule[];
97
+ /** Convert a `DurationLike` to milliseconds. */
98
+ export declare function durationToMs(d: DurationLike): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusts/limiter",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Rate limiting (fixed / sliding / token-bucket)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,20 +12,15 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": [
16
- "dist",
17
- "README.md"
18
- ],
15
+ "files": ["dist", "README.md"],
19
16
  "scripts": {
20
17
  "build": "bun run ../../build.ts"
21
18
  },
22
- "keywords": [
23
- "nexusts",
24
- "framework",
25
- "bun"
26
- ],
19
+ "keywords": ["nexusts", "framework", "bun"],
27
20
  "license": "MIT",
21
+
22
+
28
23
  "dependencies": {
29
- "@nexusts/core": "^0.7.0"
24
+ "@nexusts/core": "file:../core"
30
25
  }
31
26
  }