@spfn/core 0.2.0-beta.9 → 0.3.0-beta.10
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/LICENSE +1 -1
- package/README.md +693 -279
- package/dist/app-config/index.d.ts +96 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +430 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/boss-BiDCbbAQ.d.ts +337 -0
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +576 -30
- package/dist/config/index.js +136 -17
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +221 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +704 -94
- package/dist/db/index.js +1105 -260
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-CVKgqo8S.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +55 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +40 -7
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +55 -3
- package/dist/event/sse/client.js +164 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +265 -74
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +352 -10
- package/dist/middleware/index.js +1079 -13
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +39 -3
- package/dist/nextjs/index.js +82 -29
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +69 -52
- package/dist/nextjs/server.js +140 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +208 -0
- package/dist/ops/index.js +800 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +10 -694
- package/dist/route/index.js +112 -23
- package/dist/route/index.js.map +1 -1
- package/dist/route-builder-2ani2jEI.d.ts +553 -0
- package/dist/router-DJdpwuB6.d.ts +127 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +619 -31
- package/dist/server/index.js +2583 -360
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-ClQVomgV.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/docs/file-upload.md +195 -333
- package/package.json +250 -208
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +741 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/dist/boss-DI1r4kTS.d.ts +0 -244
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -307
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Context, Next } from 'hono';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Error Handler Middleware
|
|
5
|
-
*
|
|
6
|
-
* Handles SerializableError with automatic serialization and standard errors
|
|
7
|
-
*/
|
|
1
|
+
import { Context, Next, MiddlewareHandler } from 'hono';
|
|
2
|
+
import { N as NamedMiddlewareFactory, a as NamedMiddleware } from '../define-middleware-CVKgqo8S.js';
|
|
3
|
+
import { Redis, Cluster } from 'ioredis';
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* Options for ErrorHandler middleware
|
|
@@ -26,6 +22,43 @@ interface ErrorHandlerOptions {
|
|
|
26
22
|
* @default true
|
|
27
23
|
*/
|
|
28
24
|
enableLogging?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Attach the `error` envelope to every error response
|
|
27
|
+
*
|
|
28
|
+
* The envelope is `{ code, message, requestId }` sitting next to the
|
|
29
|
+
* existing `__type` and `message` fields, never replacing them: a web
|
|
30
|
+
* client restores its error class from `__type` while a generated client
|
|
31
|
+
* in another language classifies by `code` alone and cannot read a
|
|
32
|
+
* discriminator it has no registry for.
|
|
33
|
+
*
|
|
34
|
+
* Turn it off only to keep error bodies byte-identical to an older
|
|
35
|
+
* release. A route reached by a generated client needs it.
|
|
36
|
+
*
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
errorEnvelope?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Callback invoked when an error occurs
|
|
42
|
+
*
|
|
43
|
+
* Called asynchronously without blocking the response.
|
|
44
|
+
* Useful for external error notifications (Slack, PagerDuty, etc.)
|
|
45
|
+
*/
|
|
46
|
+
onError?: (err: Error, context: OnErrorContext) => Promise<void> | void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Context passed to onError callback
|
|
50
|
+
*/
|
|
51
|
+
interface OnErrorContext {
|
|
52
|
+
statusCode: number;
|
|
53
|
+
path: string;
|
|
54
|
+
method: string;
|
|
55
|
+
requestId?: string;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
userId?: string;
|
|
58
|
+
request: {
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
query: Record<string, string>;
|
|
61
|
+
};
|
|
29
62
|
}
|
|
30
63
|
/**
|
|
31
64
|
* Error handler middleware for Hono
|
|
@@ -34,6 +67,10 @@ interface ErrorHandlerOptions {
|
|
|
34
67
|
* SerializableError instances are serialized using their toJSON() method,
|
|
35
68
|
* preserving custom fields like `resource`, `fields`, etc.
|
|
36
69
|
*
|
|
70
|
+
* Every response also carries an `error` envelope — `{ code, message,
|
|
71
|
+
* requestId }` — for clients that classify by a single documented field
|
|
72
|
+
* instead of restoring an error class from `__type`.
|
|
73
|
+
*
|
|
37
74
|
* @param options - Configuration options
|
|
38
75
|
* @returns Error handler function for Hono's onError hook
|
|
39
76
|
*
|
|
@@ -53,7 +90,8 @@ interface ErrorHandlerOptions {
|
|
|
53
90
|
* // Throw SerializableError in routes
|
|
54
91
|
* app.get('/users/:id', (c) => {
|
|
55
92
|
* throw new NotFoundError({ message: 'User not found', resource: 'User' });
|
|
56
|
-
* // Response: { __type: 'NotFoundError', message: 'User not found', resource: 'User'
|
|
93
|
+
* // Response: { __type: 'NotFoundError', message: 'User not found', resource: 'User',
|
|
94
|
+
* // error: { code: 'NotFoundError', message: 'User not found', requestId: '…' } }
|
|
57
95
|
* });
|
|
58
96
|
* ```
|
|
59
97
|
*/
|
|
@@ -68,7 +106,11 @@ interface RequestLoggerOptions {
|
|
|
68
106
|
*
|
|
69
107
|
* Supports exact match and prefix match (e.g., '/health' excludes '/health/db').
|
|
70
108
|
*
|
|
71
|
-
*
|
|
109
|
+
* `/_core/health` is where the built-in health endpoint always answers, so a
|
|
110
|
+
* readiness probe usually points there rather than at `/health`. Leaving it
|
|
111
|
+
* out of the default fills the log with one line per probe interval.
|
|
112
|
+
*
|
|
113
|
+
* @default ['/_core/health', '/health', '/ping', '/favicon.ico']
|
|
72
114
|
*
|
|
73
115
|
* @example
|
|
74
116
|
* ```typescript
|
|
@@ -139,4 +181,304 @@ declare function maskSensitiveData(obj: any, sensitiveFields: string[], seen?: W
|
|
|
139
181
|
*/
|
|
140
182
|
declare function RequestLogger(options?: RequestLoggerOptions): (c: Context, next: Next) => Promise<void>;
|
|
141
183
|
|
|
142
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Proxy-guard middleware
|
|
186
|
+
*
|
|
187
|
+
* Verifies that an inbound request came through a trusted Next.js RPC proxy
|
|
188
|
+
* (HMAC signature) and/or from an allowed browser origin, then tags the request
|
|
189
|
+
* with a `clientType`. Lets the backend reject direct-to-backend calls that
|
|
190
|
+
* bypass the proxy.
|
|
191
|
+
*
|
|
192
|
+
* This does NOT replace user authentication (JWT) — it answers "what kind of
|
|
193
|
+
* client is this?", not "who is the user?". It also cannot stop a client from
|
|
194
|
+
* calling the proxy itself; that is the web's inherent limit (see
|
|
195
|
+
* PROXY-BACKEND-AUTH-SPEC.md). Mobile clients call the backend directly and are
|
|
196
|
+
* handled by a future attestation path, OR'd into this same check.
|
|
197
|
+
*
|
|
198
|
+
* @module middleware/proxy-guard
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* How the request reached the backend.
|
|
203
|
+
* - `web`: verified via a trusted proxy signature
|
|
204
|
+
* - `untrusted`: could not be verified (only reached handlers in `tag` mode)
|
|
205
|
+
* - future: `ios` | `android` once attestation lands
|
|
206
|
+
*/
|
|
207
|
+
type ClientType = 'web' | 'untrusted' | (string & {});
|
|
208
|
+
/**
|
|
209
|
+
* Enforcement mode.
|
|
210
|
+
* - `off`: middleware is a no-op (default — keeps existing apps working).
|
|
211
|
+
* - `tag`: verify and set `clientType`, but never reject (observe first).
|
|
212
|
+
* - `strict`: reject requests that fail verification.
|
|
213
|
+
*/
|
|
214
|
+
type ProxyGuardMode = 'off' | 'tag' | 'strict';
|
|
215
|
+
/**
|
|
216
|
+
* Optional replay store. Returns `true` when the nonce is fresh (and records it),
|
|
217
|
+
* `false` when it has been seen before. Backed by Redis in multi-instance setups.
|
|
218
|
+
*/
|
|
219
|
+
interface NonceStore {
|
|
220
|
+
checkAndSet(nonce: string, ttlMs: number): Promise<boolean>;
|
|
221
|
+
}
|
|
222
|
+
interface ProxyGuardConfig {
|
|
223
|
+
/**
|
|
224
|
+
* Active shared HMAC secret, as `<keyId>:<secret>` (or a bare secret).
|
|
225
|
+
* Defaults to `env.SPFN_PROXY_SECRET`. Without it (and no previous keys),
|
|
226
|
+
* signatures cannot be verified and every request is tagged `untrusted`.
|
|
227
|
+
*/
|
|
228
|
+
secret?: string;
|
|
229
|
+
/**
|
|
230
|
+
* Previous (grace) keys still accepted for verification during rotation, as a
|
|
231
|
+
* comma-separated list of `<keyId>:<secret>`. The proxy never signs with
|
|
232
|
+
* these — they exist only so in-flight requests signed with the prior key
|
|
233
|
+
* keep verifying until the rollout settles. Defaults to
|
|
234
|
+
* `env.SPFN_PROXY_SECRET_PREVIOUS`. Backend-only, so it can live in
|
|
235
|
+
* `.env.server` (never exposed to the Next.js process).
|
|
236
|
+
*/
|
|
237
|
+
previousSecrets?: string;
|
|
238
|
+
/** Enforcement mode. @default 'off' */
|
|
239
|
+
mode?: ProxyGuardMode;
|
|
240
|
+
/** Allowed clock skew / replay window in ms. @default 30000 */
|
|
241
|
+
windowMs?: number;
|
|
242
|
+
/**
|
|
243
|
+
* Browser origin allowlist. When set, a request carrying an `Origin` header
|
|
244
|
+
* not in this list is rejected (strict) / tagged (tag). Requests without an
|
|
245
|
+
* `Origin` (server-to-server, mobile) skip this check and fall back to the
|
|
246
|
+
* signature.
|
|
247
|
+
*/
|
|
248
|
+
allowedOrigins?: string[];
|
|
249
|
+
/** Optional nonce store for hard replay rejection. Redis (multi-instance) or in-memory (single). */
|
|
250
|
+
nonceStore?: NonceStore;
|
|
251
|
+
/**
|
|
252
|
+
* When the nonce store throws (e.g. Redis down), reject the request instead of
|
|
253
|
+
* falling back to the timestamp window. Trades availability for strictness.
|
|
254
|
+
* @default false (fall back to the timestamp window)
|
|
255
|
+
*/
|
|
256
|
+
nonceFailClosed?: boolean;
|
|
257
|
+
/** Paths to skip entirely (e.g. health checks). */
|
|
258
|
+
skipPaths?: string[];
|
|
259
|
+
/**
|
|
260
|
+
* Reject (413) once the streamed body exceeds this many bytes. Measured AS IT
|
|
261
|
+
* STREAMS — Content-Length is NOT trusted, so a missing/chunked/under-reported
|
|
262
|
+
* length can't bypass it. Bounds the per-request memory the guard buffers.
|
|
263
|
+
* Undefined = no cap (default). Multipart is exempt (unsigned).
|
|
264
|
+
*/
|
|
265
|
+
maxBodyBytes?: number;
|
|
266
|
+
}
|
|
267
|
+
declare module 'hono' {
|
|
268
|
+
interface ContextVariableMap {
|
|
269
|
+
clientType?: ClientType;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Create the proxy-guard middleware.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* export default defineServerConfig()
|
|
278
|
+
* .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })
|
|
279
|
+
* .routes(appRouter)
|
|
280
|
+
* .build();
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
declare function createProxyGuard(config?: ProxyGuardConfig): MiddlewareHandler;
|
|
284
|
+
/** Minimal cache client (compatible with ioredis Redis | Cluster). */
|
|
285
|
+
type CacheClient = {
|
|
286
|
+
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Build a Redis-backed nonce store for hard replay rejection.
|
|
290
|
+
*
|
|
291
|
+
* Uses `SET key 1 PX ttl NX` — an atomic "set if absent". A fresh nonce returns
|
|
292
|
+
* 'OK'; a replayed one returns null. TTL keeps the key set bounded (only nonces
|
|
293
|
+
* within the replay window matter).
|
|
294
|
+
*/
|
|
295
|
+
declare function createCacheNonceStore(cache: CacheClient, prefix?: string): NonceStore;
|
|
296
|
+
/**
|
|
297
|
+
* In-process nonce store for hard replay rejection without Redis.
|
|
298
|
+
*
|
|
299
|
+
* Good enough for a single instance — each process only knows its own nonces, so
|
|
300
|
+
* across multiple instances a replay routed to a different pod isn't caught. Use a
|
|
301
|
+
* cache-backed store (createCacheNonceStore) for multi-instance hard rejection.
|
|
302
|
+
* Entries self-expire after their TTL; the map is swept opportunistically so it
|
|
303
|
+
* stays bounded by the request rate within the replay window.
|
|
304
|
+
*/
|
|
305
|
+
declare function createInMemoryNonceStore(): NonceStore;
|
|
306
|
+
|
|
307
|
+
/** Test seam: forget every process-local window. */
|
|
308
|
+
declare function resetMemoryRateLimitStore(): void;
|
|
309
|
+
/**
|
|
310
|
+
* One identity dimension to limit on. A bare string uses the top-level `limit`;
|
|
311
|
+
* an object can carry its own `limit` so dimensions can differ (e.g. a loose
|
|
312
|
+
* per-IP cap alongside a tight per-account cap).
|
|
313
|
+
*/
|
|
314
|
+
type RateLimitDimension = string | {
|
|
315
|
+
key: string;
|
|
316
|
+
limit?: number;
|
|
317
|
+
};
|
|
318
|
+
interface RateLimitOptions {
|
|
319
|
+
/** Max requests allowed per window, applied to each dimension without its own `limit`. */
|
|
320
|
+
limit: number;
|
|
321
|
+
/** Window length in milliseconds. */
|
|
322
|
+
windowMs: number;
|
|
323
|
+
/**
|
|
324
|
+
* Namespace for the counter keys. Defaults to `${method} ${routePath}` so
|
|
325
|
+
* each route is limited independently.
|
|
326
|
+
*/
|
|
327
|
+
scope?: string;
|
|
328
|
+
/**
|
|
329
|
+
* Identity dimensions to limit on. Each non-empty value is counted
|
|
330
|
+
* separately and the strictest wins (e.g. by IP and by account). A dimension
|
|
331
|
+
* may override the top-level `limit` via `{ key, limit }`. Defaults to the
|
|
332
|
+
* client IP only.
|
|
333
|
+
*/
|
|
334
|
+
by?: (c: Context) => (RateLimitDimension | null | undefined)[] | Promise<(RateLimitDimension | null | undefined)[]>;
|
|
335
|
+
/**
|
|
336
|
+
* Refuse with 429 when the shared cache is unavailable, instead of counting
|
|
337
|
+
* in this process. For a surface where a per-process count is not an
|
|
338
|
+
* acceptable substitute for a shared one.
|
|
339
|
+
*/
|
|
340
|
+
failClosed?: boolean;
|
|
341
|
+
/** Message for the 429 response. */
|
|
342
|
+
message?: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Best-effort client IP for rate-limit keying.
|
|
346
|
+
*
|
|
347
|
+
* Prefers the proxy-forwarded real client IP, but ONLY when proxy-guard verified
|
|
348
|
+
* the request came through our proxy (`clientType` set and not 'untrusted') — on a
|
|
349
|
+
* verified request the proxy is the sole hop, so the value is the true client and
|
|
350
|
+
* isn't client-spoofable. Otherwise (no proxy-guard, or unverified/direct request)
|
|
351
|
+
* the forwarded header is attacker-settable, so fall back to the raw chain — whose
|
|
352
|
+
* leftmost hop is itself spoofable, so still pair with an account/target dimension
|
|
353
|
+
* for anything security-sensitive.
|
|
354
|
+
*
|
|
355
|
+
* Last resort before giving up is the TCP peer address from the Node adapter
|
|
356
|
+
* socket. Without it, a deployment that sets no forwarding header would collapse
|
|
357
|
+
* every client onto a single `'unknown'` bucket.
|
|
358
|
+
*/
|
|
359
|
+
declare function getClientIp(c: Context): string;
|
|
360
|
+
/**
|
|
361
|
+
* Fixed-window rate limiter — counters in the shared cache when there is one,
|
|
362
|
+
* in this process otherwise.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* route.post('/_auth/login')
|
|
367
|
+
* .use([rateLimit({ limit: 10, windowMs: 60_000 })])
|
|
368
|
+
* .handler(...);
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
declare const rateLimit: NamedMiddlewareFactory<"rateLimit", [options: RateLimitOptions]>;
|
|
372
|
+
/** Set the fail-closed default for named policies. Called by the server at boot. */
|
|
373
|
+
declare function setRateLimitFailClosedDefault(failClosed: boolean): void;
|
|
374
|
+
/**
|
|
375
|
+
* Replace the named-policy registry. Called by the server at boot; passing
|
|
376
|
+
* undefined clears it (so a restart without policies doesn't keep stale ones).
|
|
377
|
+
*/
|
|
378
|
+
declare function setRateLimitPolicies(policies?: Record<string, RateLimitOptions>): void;
|
|
379
|
+
/** Look up a configured policy by name — undefined when the app didn't set it. */
|
|
380
|
+
declare function getRateLimitPolicy(name: string): RateLimitOptions | undefined;
|
|
381
|
+
/**
|
|
382
|
+
* Rate-limit a route under a named policy.
|
|
383
|
+
*
|
|
384
|
+
* A package author tags a sensitive route with a policy name plus a safe
|
|
385
|
+
* fallback; the consuming app tunes the numbers centrally via
|
|
386
|
+
* `defineServerConfig().rateLimit({ policies: { [name]: {...} } })`. When the
|
|
387
|
+
* app configures the policy, its fields override the fallback (shallow merge);
|
|
388
|
+
* otherwise the fallback applies, so the route is protected out of the box.
|
|
389
|
+
*
|
|
390
|
+
* LAYERS on top of the global default limiter rather than replacing it: the tag
|
|
391
|
+
* has a distinct name (`rateLimit:<name>`) and does not skip the global, so a
|
|
392
|
+
* route gets both the global per-IP floor (when enabled) and this policy's own
|
|
393
|
+
* bucket — whichever is stricter trips first. Opt out of the global floor on a
|
|
394
|
+
* route with `.skip(['rateLimit'])`.
|
|
395
|
+
*
|
|
396
|
+
* The counter scope defaults to the policy name, so (a) every route sharing a
|
|
397
|
+
* policy shares one bucket, and (b) the key never collides with the global
|
|
398
|
+
* default's per-route `${method} ${path}` scope.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* route.post('/_auth/login')
|
|
403
|
+
* .use([rateLimitPolicy('auth-login', { limit: 5, windowMs: 60_000 })])
|
|
404
|
+
* .handler(...);
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
declare function rateLimitPolicy(name: string, fallback: RateLimitOptions): NamedMiddleware<string>;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The rate limiter's fixed-window counter as a pluggable store — the same
|
|
411
|
+
* pattern the clientProofV1 replay ledger and the SSE token manager already
|
|
412
|
+
* use: an in-memory default, a Redis/Valkey store on top of `getCache()`, and
|
|
413
|
+
* the cache's presence deciding which one a request gets.
|
|
414
|
+
*
|
|
415
|
+
* Why the memory default exists: without it, "no cache configured" and "cache
|
|
416
|
+
* down" both meant the limiter had nowhere to count, so the only choices were
|
|
417
|
+
* letting every request through or refusing every request. Neither is what a
|
|
418
|
+
* limiter is for. Counting in the process is worse than counting in Redis and
|
|
419
|
+
* far better than not counting.
|
|
420
|
+
*
|
|
421
|
+
* What the memory store cannot do is span processes. Behind N instances each
|
|
422
|
+
* keeps its own counters, so the effective limit is N times the configured one.
|
|
423
|
+
* That is the cost of the fallback and the reason a real deployment still
|
|
424
|
+
* points `CACHE_URL` at Redis.
|
|
425
|
+
*
|
|
426
|
+
* @module middleware/rate-limit-store
|
|
427
|
+
*/
|
|
428
|
+
|
|
429
|
+
/** One window's state for a key: how many hits, and how long the window has left. */
|
|
430
|
+
interface RateLimitHit {
|
|
431
|
+
count: number;
|
|
432
|
+
/** Milliseconds until the window resets. */
|
|
433
|
+
pttl: number;
|
|
434
|
+
}
|
|
435
|
+
/** What the limiter needs from wherever the counters live. */
|
|
436
|
+
interface RateLimitStore {
|
|
437
|
+
/** Records one hit against `key` and answers the window's state after it. */
|
|
438
|
+
hit(key: string, windowMs: number): Promise<RateLimitHit>;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Counters in Redis — shared by every instance, so the configured limit is the
|
|
442
|
+
* limit no matter how many processes serve the traffic.
|
|
443
|
+
*/
|
|
444
|
+
declare class CacheRateLimitStore implements RateLimitStore {
|
|
445
|
+
private readonly cache;
|
|
446
|
+
constructor(cache: Redis | Cluster);
|
|
447
|
+
hit(key: string, windowMs: number): Promise<RateLimitHit>;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Counters in this process.
|
|
451
|
+
*
|
|
452
|
+
* Entries are pruned lazily — a key past its window resets on its next hit, so
|
|
453
|
+
* the common path stays O(1). Keys nobody touches again would otherwise sit
|
|
454
|
+
* there forever, so a full sweep runs once the map crosses `maxKeys`; that
|
|
455
|
+
* bound is what keeps a limiter keyed by client IP from growing without end.
|
|
456
|
+
*/
|
|
457
|
+
declare class MemoryRateLimitStore implements RateLimitStore {
|
|
458
|
+
private readonly maxKeys;
|
|
459
|
+
private readonly windows;
|
|
460
|
+
/** How many live windows were dropped to stay under `maxKeys`. */
|
|
461
|
+
private evicted;
|
|
462
|
+
constructor(maxKeys?: number);
|
|
463
|
+
hit(key: string, windowMs: number): Promise<RateLimitHit>;
|
|
464
|
+
/** Drops every window that has already reset. */
|
|
465
|
+
prune(nowMillis: number): void;
|
|
466
|
+
/**
|
|
467
|
+
* Frees a slot for a new key.
|
|
468
|
+
*
|
|
469
|
+
* Expired windows go first. If every window is still live the oldest one is
|
|
470
|
+
* dropped, because a hard bound is the only thing standing between a
|
|
471
|
+
* per-IP limiter and unbounded memory. A dropped window loses its count,
|
|
472
|
+
* so a caller who can mint `maxKeys` distinct identities can push another
|
|
473
|
+
* one out — which is why `evictionCount` is worth watching: it says this
|
|
474
|
+
* process is past what an in-memory limiter should be asked to hold, and
|
|
475
|
+
* the deployment wants a real cache.
|
|
476
|
+
*/
|
|
477
|
+
private makeRoom;
|
|
478
|
+
get size(): number;
|
|
479
|
+
/** Live windows dropped for capacity — nonzero means the bound is binding. */
|
|
480
|
+
get evictionCount(): number;
|
|
481
|
+
clear(): void;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export { CacheRateLimitStore, type ClientType, ErrorHandler, type ErrorHandlerOptions, MemoryRateLimitStore, type NonceStore, type OnErrorContext, type ProxyGuardConfig, type ProxyGuardMode, type RateLimitDimension, type RateLimitHit, type RateLimitOptions, type RateLimitStore, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData, rateLimit, rateLimitPolicy, resetMemoryRateLimitStore, setRateLimitFailClosedDefault, setRateLimitPolicies };
|