@nifrajs/middleware 2.2.0 → 2.4.0

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.
Files changed (56) hide show
  1. package/README.md +22 -22
  2. package/dist/_utils.d.ts +3 -3
  3. package/dist/_utils.js +3 -3
  4. package/dist/admission.d.ts +2 -2
  5. package/dist/admission.js +2 -2
  6. package/dist/cache-control.d.ts +1 -1
  7. package/dist/cache-control.js +1 -1
  8. package/dist/cache.js +1 -1
  9. package/dist/compression.d.ts +1 -1
  10. package/dist/compression.js +4 -4
  11. package/dist/cors.d.ts +1 -1
  12. package/dist/cors.js +2 -2
  13. package/dist/durable-command.d.ts +34 -0
  14. package/dist/durable-command.d.ts.map +1 -0
  15. package/dist/durable-command.js +50 -0
  16. package/dist/durable-command.js.map +1 -0
  17. package/dist/etag.d.ts +2 -2
  18. package/dist/etag.js +3 -3
  19. package/dist/healthcheck.d.ts +2 -2
  20. package/dist/healthcheck.js +1 -1
  21. package/dist/idempotency.d.ts +4 -4
  22. package/dist/idempotency.js +3 -3
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/logger.d.ts +2 -2
  28. package/dist/logger.js +2 -2
  29. package/dist/openapi.d.ts +4 -4
  30. package/dist/openapi.js +4 -4
  31. package/dist/rate-limit.d.ts +8 -8
  32. package/dist/rate-limit.js +5 -5
  33. package/dist/request-id.d.ts +1 -1
  34. package/dist/request-id.js +1 -1
  35. package/dist/security-headers.d.ts +1 -1
  36. package/dist/security-headers.js +1 -1
  37. package/dist/token-auth.d.ts +6 -6
  38. package/dist/token-auth.js +4 -4
  39. package/package.json +4 -4
  40. package/src/_utils.ts +3 -3
  41. package/src/admission.ts +4 -4
  42. package/src/cache-control.ts +1 -1
  43. package/src/cache.ts +1 -1
  44. package/src/compression.ts +4 -4
  45. package/src/cors.ts +2 -2
  46. package/src/durable-command.ts +62 -0
  47. package/src/etag.ts +4 -4
  48. package/src/healthcheck.ts +3 -3
  49. package/src/idempotency.ts +7 -7
  50. package/src/index.ts +3 -2
  51. package/src/logger.ts +2 -2
  52. package/src/openapi.ts +8 -8
  53. package/src/rate-limit.ts +10 -10
  54. package/src/request-id.ts +1 -1
  55. package/src/security-headers.ts +2 -2
  56. package/src/token-auth.ts +9 -9
package/src/openapi.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { definePlugin } from "@nifrajs/core/server"
2
- // The OpenAPI generator only — `@nifrajs/schema/openapi` is pure (no TypeBox runtime), so the openapi()
2
+ // The OpenAPI generator only - `@nifrajs/schema/openapi` is pure (no TypeBox runtime), so the openapi()
3
3
  // plugin gets full `t`-schema introspection without pulling the `t` builder into every consumer.
4
4
  import { toOpenAPI } from "@nifrajs/schema/openapi"
5
5
 
6
- /** A registered route as seen by {@link buildOpenApiDocument} — structurally a `@nifrajs/core`
6
+ /** A registered route as seen by {@link buildOpenApiDocument} - structurally a `@nifrajs/core`
7
7
  * `RouteDescriptor` (so `app.routes()` is passed straight through). */
8
8
  export interface RouteLike {
9
9
  readonly method: string
10
10
  readonly path: string
11
- // `| undefined` (not just `?`) so a core `RouteDescriptor` — whose `schema` is a *required*
12
- // `RouteSchema | undefined` — is assignable under `exactOptionalPropertyTypes`.
11
+ // `| undefined` (not just `?`) so a core `RouteDescriptor` - whose `schema` is a *required*
12
+ // `RouteSchema | undefined` - is assignable under `exactOptionalPropertyTypes`.
13
13
  readonly schema?:
14
14
  | { readonly body?: unknown; readonly query?: unknown; readonly response?: unknown }
15
15
  | undefined
@@ -57,7 +57,7 @@ export interface OpenApiOptions {
57
57
  readonly path?: string
58
58
  /** Exclude routes from the document (the doc path itself is always excluded). */
59
59
  readonly exclude?: (route: { readonly method: string; readonly path: string }) => boolean
60
- /** Per-operation overrides keyed by `"GET /users/:id"`, shallow-merged over the generated skeleton —
60
+ /** Per-operation overrides keyed by `"GET /users/:id"`, shallow-merged over the generated skeleton -
61
61
  * the escape hatch for rich request/response schemas, tags, and per-op security (Standard Schema
62
62
  * can't be introspected). */
63
63
  readonly operations?: Readonly<Record<string, Record<string, unknown>>>
@@ -127,10 +127,10 @@ function scalarPage(specUrl: string, title: string, cdn: string): string {
127
127
  }
128
128
 
129
129
  /**
130
- * Serve an OpenAPI 3.1 document (a structural subset — see {@link buildOpenApiDocument}) at
130
+ * Serve an OpenAPI 3.1 document (a structural subset - see {@link buildOpenApiDocument}) at
131
131
  * `options.path` (default `/openapi.json`), generated from the app's registered routes. Generation is
132
132
  * **lazy + memoized**: it reads `app.routes()` on the first request, by which point every route is
133
- * registered — so the plugin's own position in the chain doesn't matter.
133
+ * registered - so the plugin's own position in the chain doesn't matter.
134
134
  *
135
135
  * Pass `ui: true` (or `ui: { path, title, cdn }`) to also serve a Scalar API-reference page (default
136
136
  * `/reference`) that renders the spec. The page loads Scalar from a CDN; pin/self-host via `ui.cdn`,
@@ -149,7 +149,7 @@ export function openapi(options: OpenApiOptions = {}) {
149
149
  let uiCached: string | undefined
150
150
  return definePlugin("openapi", (app) => {
151
151
  // Capture `app` (the same mutable builder every later `.get()`/`.post()` registers on) and read its
152
- // routes lazily, at request time — so all routes exist regardless of where the plugin sits.
152
+ // routes lazily, at request time - so all routes exist regardless of where the plugin sits.
153
153
  app.register("GET", docPath, undefined, () => {
154
154
  cached ??= JSON.stringify(
155
155
  buildOpenApiDocument(app.routes(), {
package/src/rate-limit.ts CHANGED
@@ -10,7 +10,7 @@ export interface RateLimitResult {
10
10
 
11
11
  /**
12
12
  * Counter backend. Production deploys MUST use a shared store (Redis, etc.) so the
13
- * limit holds across instances — that's a user dependency, not ours, hence the
13
+ * limit holds across instances - that's a user dependency, not ours, hence the
14
14
  * interface. {@link MemoryStore} is for dev / single-instance only.
15
15
  */
16
16
  export interface RateLimitStore {
@@ -18,7 +18,7 @@ export interface RateLimitStore {
18
18
  }
19
19
 
20
20
  export interface MemoryStoreOptions {
21
- /** Allow the in-memory store in production. Off by default — a per-instance limiter is unsafe across instances. */
21
+ /** Allow the in-memory store in production. Off by default - a per-instance limiter is unsafe across instances. */
22
22
  readonly allowInProduction?: boolean
23
23
  /** Hard cap on tracked client keys; expired keys are evicted first, then oldest active keys. Default `100_000`.
24
24
  * Bounds memory against an unbounded key space (bot scans, per-IP buckets). */
@@ -29,7 +29,7 @@ export interface MemoryStoreOptions {
29
29
 
30
30
  /** Max entries scanned (oldest-first) per eviction looking for an expired victim before falling back
31
31
  * to evicting the oldest-inserted. Bounds eviction to O(1) per insertion instead of a full O(n) sweep
32
- * under a distinct-key flood — the abuse the key cap defends against. */
32
+ * under a distinct-key flood - the abuse the key cap defends against. */
33
33
  const MAX_EVICTION_SCAN = 64
34
34
 
35
35
  /**
@@ -38,8 +38,8 @@ const MAX_EVICTION_SCAN = 64
38
38
  * Bounded against unbounded growth: expired windows are swept lazily (amortized, at most once per
39
39
  * `sweepIntervalMs`) and the key set is hard-capped at `maxKeys` (expired keys are evicted first,
40
40
  * then oldest active keys).
41
- * Without this, expired entries for keys never seen again — and the unbounded key space of a bot scan
42
- * — would accumulate in the map forever and OOM a single-instance deploy.
41
+ * Without this, expired entries for keys never seen again - and the unbounded key space of a bot scan
42
+ * - would accumulate in the map forever and OOM a single-instance deploy.
43
43
  */
44
44
  export class MemoryStore implements RateLimitStore {
45
45
  private readonly windows = new Map<string, { count: number; resetAt: number }>()
@@ -48,7 +48,7 @@ export class MemoryStore implements RateLimitStore {
48
48
  private lastSweep = 0
49
49
  private evictionScans = 0
50
50
 
51
- /** Cumulative entries inspected by the eviction scan since construction — an eviction-pressure
51
+ /** Cumulative entries inspected by the eviction scan since construction - an eviction-pressure
52
52
  * gauge. The bounded scan keeps this ~O(1) per over-cap insert (≤ {@link MAX_EVICTION_SCAN}); a
53
53
  * regressed full O(n) sweep would make it ~maxKeys per insert (what the regression test asserts). */
54
54
  get evictionScanCount(): number {
@@ -98,7 +98,7 @@ export class MemoryStore implements RateLimitStore {
98
98
  private enforceMaxKeys(now: number): void {
99
99
  if (this.windows.size <= this.maxKeys) return
100
100
  // Prefer evicting an expired window over an active user's, but bound the work. A full sweep on
101
- // every insertion is O(n)/request under a distinct-key flood — the exact abuse the cap defends
101
+ // every insertion is O(n)/request under a distinct-key flood - the exact abuse the cap defends
102
102
  // against. Scan only a small fixed budget (oldest-first, where expired entries cluster) for a
103
103
  // victim; if none is found, evict the oldest-inserted (O(1)). Expired entries beyond the budget
104
104
  // are still reclaimed by the amortized sweep in `hit`.
@@ -132,13 +132,13 @@ export interface RateLimitOptions {
132
132
  * Default `0`.
133
133
  *
134
134
  * The default key reads the client IP from `X-Forwarded-For` as the address your **edge** proxy
135
- * observed — the entry `trustedProxies` from the right (1 proxy → the rightmost hop; 2 → the
135
+ * observed - the entry `trustedProxies` from the right (1 proxy → the rightmost hop; 2 → the
136
136
  * second-from-right; …). Your proxies append on the right, so a client can only inject fake hops on
137
137
  * the *left*, which this skips → not spoofable when `trustedProxies` matches your topology.
138
138
  *
139
139
  * ⚠️ With the default `0`, `X-Forwarded-For` is treated as fully client-controlled and **ignored**.
140
140
  * Reading the
141
- * *first* XFF hop — the old behavior — let any client mint a fresh bucket per request and defeat the
141
+ * *first* XFF hop - the old behavior - let any client mint a fresh bucket per request and defeat the
142
142
  * limiter. Set `trustedProxies` (only safe behind a proxy you control that appends XFF), configure a
143
143
  * trusted single-IP {@link header}, or supply a custom {@link key} (e.g. an authenticated user id).
144
144
  */
@@ -151,7 +151,7 @@ export interface RateLimitOptions {
151
151
  */
152
152
  readonly allowGlobalKey?: boolean
153
153
  /**
154
- * Bucket key for a request. Overrides the default XFF-based key entirely — set this for accurate
154
+ * Bucket key for a request. Overrides the default XFF-based key entirely - set this for accurate
155
155
  * per-client limiting (e.g. an authenticated user id, or a header your proxy sets). A `Middleware`
156
156
  * can't see the socket IP (that needs the server instance).
157
157
  */
package/src/request-id.ts CHANGED
@@ -10,7 +10,7 @@ export interface RequestIdOptions {
10
10
  /**
11
11
  * A {@link definePlugin} plugin that gives every request a stable id: it reuses an inbound
12
12
  * `x-request-id` (or generates one), exposes it on the handler context as **`c.requestId`** (typed,
13
- * threaded by `derive`), and echoes it on the response header. Idempotent — applying it twice is a
13
+ * threaded by `derive`), and echoes it on the response header. Idempotent - applying it twice is a
14
14
  * no-op.
15
15
  *
16
16
  * ```ts
@@ -3,7 +3,7 @@ import type { Middleware } from "@nifrajs/core/server"
3
3
  import { withHeaders } from "./_utils.ts"
4
4
 
5
5
  export interface SecurityHeadersOptions {
6
- /** `Strict-Transport-Security`. Off by default — opt in once you're sure you're HTTPS-only. */
6
+ /** `Strict-Transport-Security`. Off by default - opt in once you're sure you're HTTPS-only. */
7
7
  readonly hsts?: {
8
8
  readonly maxAge: number
9
9
  readonly includeSubDomains?: boolean
@@ -40,7 +40,7 @@ export function securityHeaders(options: SecurityHeadersOptions = {}): Middlewar
40
40
  {
41
41
  name: "security-headers",
42
42
  // Mutate in place on the common (mutable-headers) response; clone only for an immutable one
43
- // (see withHeaders — the old always-clone was ~3% of a realistic request).
43
+ // (see withHeaders - the old always-clone was ~3% of a realistic request).
44
44
  onResponse: (res) =>
45
45
  withHeaders(res, (headers) => {
46
46
  headers.set("X-Content-Type-Options", "nosniff")
package/src/token-auth.ts CHANGED
@@ -4,7 +4,7 @@ import { definePlugin, type NifraPlugin } from "@nifrajs/core/server"
4
4
  type MaybePromise<T> = T | Promise<T>
5
5
 
6
6
  /**
7
- * A token-auth plugin (`bearer` / `apiKey`). Apply it with `app.use(auth)` — it rejects unauthorized
7
+ * A token-auth plugin (`bearer` / `apiKey`). Apply it with `app.use(auth)` - it rejects unauthorized
8
8
  * requests to **routes defined after it** with `401` (unless `optional`). Read the verified principal
9
9
  * inside a handler/loader via {@link AuthPlugin.principal} (nullable) or
10
10
  * {@link AuthPlugin.requirePrincipal} (throws `401` when absent). The principal is verified once per
@@ -51,11 +51,11 @@ function createTokenAuth<P>(config: TokenAuthConfig<P>): AuthPlugin<P> {
51
51
  const plugin = definePlugin(config.name, (app) =>
52
52
  app.beforeHandle(async (c: { readonly req: Request }) => {
53
53
  const token = config.extract(c.req)
54
- // Empty string is treated as "no credential" — never passed to verify.
54
+ // Empty string is treated as "no credential" - never passed to verify.
55
55
  const principal = token !== undefined && token !== "" ? await config.verify(token) : null
56
56
  if (principal !== null && principal !== undefined) {
57
57
  store.set(c.req, principal)
58
- return undefined // authorized — continue to the handler
58
+ return undefined // authorized - continue to the handler
59
59
  }
60
60
  return config.optional ? undefined : reject() // 401 short-circuits (skips the handler)
61
61
  }),
@@ -79,7 +79,7 @@ function createTokenAuth<P>(config: TokenAuthConfig<P>): AuthPlugin<P> {
79
79
 
80
80
  export interface BearerOptions<P> {
81
81
  /** Verify a bearer token → a principal (truthy) or `null`/`undefined` (rejected). May be async
82
- * (DB/JWT lookup). For a constant-secret comparison, use a constant-time compare — never `===`. */
82
+ * (DB/JWT lookup). For a constant-secret comparison, use a constant-time compare - never `===`. */
83
83
  readonly verify: (token: string) => MaybePromise<P | null | undefined>
84
84
  /** When `true`, requests without a valid token pass through (`principal` is `null`) instead of `401`. */
85
85
  readonly optional?: boolean
@@ -90,7 +90,7 @@ export interface BearerOptions<P> {
90
90
  /**
91
91
  * `Authorization: Bearer <token>` authentication. Parses the header, runs `verify`, and rejects with
92
92
  * `401` (+ `WWW-Authenticate: Bearer`) when the token is missing/invalid (unless `optional`). The
93
- * verified principal is read via the returned instance — see {@link AuthPlugin}.
93
+ * verified principal is read via the returned instance - see {@link AuthPlugin}.
94
94
  */
95
95
  export function bearer<P>(options: BearerOptions<P>): AuthPlugin<P> {
96
96
  const realm = options.realm ?? "api"
@@ -140,7 +140,7 @@ function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {
140
140
  /**
141
141
  * Build a constant-time verifier for a fixed key set. Each valid key is hashed once (SHA-256, fixed
142
142
  * 32 bytes); per request the candidate is hashed and compared against every digest **without early
143
- * exit**, so timing depends only on the (public) key count — never on the secret value or which key
143
+ * exit**, so timing depends only on the (public) key count - never on the secret value or which key
144
144
  * matched. Portable across all runtimes via WebCrypto.
145
145
  */
146
146
  function staticKeyVerify(keys: readonly string[]): (candidate: string) => Promise<string | null> {
@@ -158,11 +158,11 @@ function staticKeyVerify(keys: readonly string[]): (candidate: string) => Promis
158
158
 
159
159
  /**
160
160
  * API-key authentication via a header (default `x-api-key`). Two forms:
161
- * - `apiKey({ keys })` — a fixed key set, compared in **constant time**; the matched key is the principal.
162
- * - `apiKey({ verify })` — custom (e.g. DB-backed) verification returning a typed principal.
161
+ * - `apiKey({ keys })` - a fixed key set, compared in **constant time**; the matched key is the principal.
162
+ * - `apiKey({ verify })` - custom (e.g. DB-backed) verification returning a typed principal.
163
163
  *
164
164
  * Rejects missing/invalid keys with `401` (unless `optional`). Read the principal via the returned
165
- * instance — see {@link AuthPlugin}.
165
+ * instance - see {@link AuthPlugin}.
166
166
  */
167
167
  export function apiKey(options: ApiKeyStaticOptions): AuthPlugin<string>
168
168
  export function apiKey<P>(options: ApiKeyVerifyOptions<P>): AuthPlugin<P>