@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 +2 -2
- package/dist/backends/drizzle.d.ts +39 -0
- package/dist/backends/index.d.ts +5 -0
- package/dist/backends/memory.d.ts +27 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -4
- package/dist/index.js.map +5 -5
- package/dist/limiter.middleware.d.ts +10 -0
- package/dist/limiter.module.d.ts +22 -0
- package/dist/limiter.service.d.ts +17 -0
- package/dist/types.d.ts +98 -0
- package/package.json +6 -11
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
|
|
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.
|
|
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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.
|
|
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": "
|
|
24
|
+
"@nexusts/core": "file:../core"
|
|
30
25
|
}
|
|
31
26
|
}
|