@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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @nifrajs/middleware
2
2
 
3
- Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
4
- limits, auth, CSRF, JWT/JWKS, IP restriction, response caching, timing, and ops helpers — applied with
3
+ Composable, dependency-light middleware for [nifra](../../README.md) - CORS, security headers, body
4
+ limits, auth, CSRF, JWT/JWKS, IP restriction, response caching, timing, and ops helpers - applied with
5
5
  `app.use()`.
6
6
 
7
7
  ```sh
@@ -33,41 +33,41 @@ const app = server()
33
33
  .get("/", () => ({ ok: true }))
34
34
  ```
35
35
 
36
- - **`bodyLimit({ maxBytes })`** — fail-closed `Content-Length` gate before routing. Lengthless
36
+ - **`bodyLimit({ maxBytes })`** - fail-closed `Content-Length` gate before routing. Lengthless
37
37
  bodies are rejected with `411` by default; use route-level `c.boundedBody()` / schema validation
38
38
  for intentionally streamed endpoints.
39
- - **`basicAuth(options)`** — Basic Auth plugin with constant-time static credential comparison or
39
+ - **`basicAuth(options)`** - Basic Auth plugin with constant-time static credential comparison or
40
40
  a custom verifier.
41
- - **`bearer(options)` / `apiKey(options)`** — token auth plugins with typed principals.
42
- - **`csrf({ secret })`** — signed double-submit CSRF protection plus Origin/Referer checking.
43
- - **`jwt(options)` + `verifyJwt()` / `tryVerifyJwt()` + `jwk()` / `jwks()`** — JWT auth
41
+ - **`bearer(options)` / `apiKey(options)`** - token auth plugins with typed principals.
42
+ - **`csrf({ secret })`** - signed double-submit CSRF protection plus Origin/Referer checking.
43
+ - **`jwt(options)` + `verifyJwt()` / `tryVerifyJwt()` + `jwk()` / `jwks()`** - JWT auth
44
44
  with explicit algorithm allowlists, required expiration by default, issuer/audience checks,
45
45
  direct JWK, HTTPS JWKS, and an additive no-throw Result helper for manual verification.
46
- - **`ipRestriction(options)`** — allow/deny IPv4/IPv6 exact and CIDR matches. Fails closed unless
46
+ - **`ipRestriction(options)`** - allow/deny IPv4/IPv6 exact and CIDR matches. Fails closed unless
47
47
  you provide `clientIp`, trusted proxy extraction, or a trusted single-IP header.
48
- - **`cors(options)`** — preflight handling + headers on *every* response (errors and
48
+ - **`cors(options)`** - preflight handling + headers on *every* response (errors and
49
49
  404s included). Origin as `"*"` / exact / list / predicate. **Throws** if
50
50
  `credentials: true` is paired with `origin: "*"` (the browser rejects it).
51
- - **`securityHeaders(options)`** — `X-Content-Type-Options`, `X-Frame-Options`,
51
+ - **`securityHeaders(options)`** - `X-Content-Type-Options`, `X-Frame-Options`,
52
52
  `Referrer-Policy` by default; opt-in HSTS and CSP.
53
- - **`rateLimit(options)`** — `429` + `Retry-After` + `RateLimit-*` headers, with a
53
+ - **`rateLimit(options)`** - `429` + `Retry-After` + `RateLimit-*` headers, with a
54
54
  pluggable `RateLimitStore`. Configure a trusted `key`, trusted single-IP `header`, or
55
55
  `trustedProxies`; a missing key source fails closed instead of silently sharing one
56
56
  bucket. The bundled `MemoryStore` **refuses to run in production** (a per-instance
57
- limiter is unsafe across instances) — provide a shared store (Redis, etc.) there.
58
- - **`cache({ store, ttlMs })` / `responseCache(...)`** — full response cache with a pluggable
57
+ limiter is unsafe across instances) - provide a shared store (Redis, etc.) there.
58
+ - **`cache({ store, ttlMs })` / `responseCache(...)`** - full response cache with a pluggable
59
59
  store, `Vary`-aware keys, `Age`, byte caps, and `Cache-Control` / `Set-Cookie` safety defaults.
60
60
  `MemoryResponseCache` is dev/single-instance only unless explicitly allowed in production.
61
- - **`timing()`** — `Server-Timing` plus typed `c.timing.metric/mark/measure` controls.
62
- - **`prettyJson()`** — capped, JSON-only pretty printing for debugging and developer-facing APIs.
63
- - **`methodOverride()`** — header/query method tunneling for clients that can only send `POST`.
61
+ - **`timing()`** - `Server-Timing` plus typed `c.timing.metric/mark/measure` controls.
62
+ - **`prettyJson()`** - capped, JSON-only pretty printing for debugging and developer-facing APIs.
63
+ - **`methodOverride()`** - header/query method tunneling for clients that can only send `POST`.
64
64
  Header override is on by default; query override is opt-in.
65
- - **`trimTrailingSlash()` / `appendTrailingSlash()`** — redirect or rewrite URL canonicalization.
66
- - **`poweredBy()`** — opt-in product/framework header; Nifra emits no powered-by header by default.
67
- - **`language()` / `pickLanguage()`** — `Accept-Language` negotiation with typed `c.language`.
68
- - **`combine()` / `namedCombine()`** — reusable runtime bundles for middleware/plugins.
65
+ - **`trimTrailingSlash()` / `appendTrailingSlash()`** - redirect or rewrite URL canonicalization.
66
+ - **`poweredBy()`** - opt-in product/framework header; Nifra emits no powered-by header by default.
67
+ - **`language()` / `pickLanguage()`** - `Accept-Language` negotiation with typed `c.language`.
68
+ - **`combine()` / `namedCombine()`** - reusable runtime bundles for middleware/plugins.
69
69
  - **`requestId()` / `logger()` / `etag()` / `compression()` / `cacheControl()` /
70
- `idempotency()` / `healthcheck()` / `openapi()`** — additional operational middleware for APIs.
70
+ `idempotency()` / `healthcheck()` / `openapi()`** - additional operational middleware for APIs.
71
71
 
72
72
  Request timeouts are configured at the core server boundary so they can abort `c.signal` and race the
73
73
  whole lifecycle:
@@ -80,7 +80,7 @@ const app = server({ requestTimeoutMs: 5_000 })
80
80
 
81
81
  ## For AI agents
82
82
 
83
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
83
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
84
84
  one cheap read instead of the whole corpus. For the wider framework: the repo's
85
85
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
86
86
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
package/dist/_utils.d.ts CHANGED
@@ -16,12 +16,12 @@ export declare function hmacSha256(input: string, secret: string | Uint8Array):
16
16
  export declare function verifyHmacSha256(input: string, signature: string, secret: string | Uint8Array): Promise<boolean>;
17
17
  /**
18
18
  * Apply header mutations to a response, in place when possible. Framework-built responses
19
- * (`new Response`, `Response.json`) have mutable headers across every runtime — so the common path
19
+ * (`new Response`, `Response.json`) have mutable headers across every runtime - so the common path
20
20
  * mutates `res` directly and returns it, allocating nothing. Only an *immutable*-headers response
21
- * (`Response.redirect()`/`Response.error()`, or a proxied `fetch()` response on Node/Deno/workerd —
21
+ * (`Response.redirect()`/`Response.error()`, or a proxied `fetch()` response on Node/Deno/workerd -
22
22
  * never on Bun) makes `.set`/`.append` throw; that path clones into a fresh `Headers` + `Response`,
23
23
  * exactly the old always-clone behavior. `apply` runs once either way (immutability is all-or-nothing
24
- * — the first mutation throws before any partial change), so a mutation chain is safe to pass.
24
+ * - the first mutation throws before any partial change), so a mutation chain is safe to pass.
25
25
  */
26
26
  export declare function withHeaders(res: Response, apply: (headers: Headers) => void): Response;
27
27
  //# sourceMappingURL=_utils.d.ts.map
package/dist/_utils.js CHANGED
@@ -120,12 +120,12 @@ export async function verifyHmacSha256(input, signature, secret) {
120
120
  }
121
121
  /**
122
122
  * Apply header mutations to a response, in place when possible. Framework-built responses
123
- * (`new Response`, `Response.json`) have mutable headers across every runtime — so the common path
123
+ * (`new Response`, `Response.json`) have mutable headers across every runtime - so the common path
124
124
  * mutates `res` directly and returns it, allocating nothing. Only an *immutable*-headers response
125
- * (`Response.redirect()`/`Response.error()`, or a proxied `fetch()` response on Node/Deno/workerd —
125
+ * (`Response.redirect()`/`Response.error()`, or a proxied `fetch()` response on Node/Deno/workerd -
126
126
  * never on Bun) makes `.set`/`.append` throw; that path clones into a fresh `Headers` + `Response`,
127
127
  * exactly the old always-clone behavior. `apply` runs once either way (immutability is all-or-nothing
128
- * — the first mutation throws before any partial change), so a mutation chain is safe to pass.
128
+ * - the first mutation throws before any partial change), so a mutation chain is safe to pass.
129
129
  */
130
130
  export function withHeaders(res, apply) {
131
131
  try {
@@ -2,7 +2,7 @@ import type { AdmissionController } from "@nifrajs/core/server";
2
2
  /**
3
3
  * Adaptive capacity admission. Rate limiting bounds request *frequency* and `@nifrajs/core/budget` bounds
4
4
  * request *duration*; neither stops a healthy instance from accepting more *concurrent* work than it
5
- * can finish. This gate admits on live capacity evidence — in-flight count + event-loop lag — briefly
5
+ * can finish. This gate admits on live capacity evidence - in-flight count + event-loop lag - briefly
6
6
  * queues at the edge, and sheds the rest with `429` + `Retry-After`, so p99 stays bounded under load
7
7
  * instead of collapsing.
8
8
  *
@@ -10,7 +10,7 @@ import type { AdmissionController } from "@nifrajs/core/server";
10
10
  * hook layers tenant priority and reserved capacity on top without leaking
11
11
  * those concerns into the OSS core.
12
12
  *
13
- * Wire it as the server's `admission` option (NOT an `onRequest` hook — a hook disables the native
13
+ * Wire it as the server's `admission` option (NOT an `onRequest` hook - a hook disables the native
14
14
  * route table). Off by default: when unset, the request path is untouched.
15
15
  */
16
16
  export type ShedReason = "inflight" | "loop-lag" | "queue-timeout" | "policy" | "cancelled";
package/dist/admission.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const OVERLOADED_BODY = JSON.stringify({ ok: false, error: "overloaded" });
2
- /** Shared no-op sampler — the fallback when an explicitly supplied monitor has no histogram. */
2
+ /** Shared no-op sampler - the fallback when an explicitly supplied monitor has no histogram. */
3
3
  const NO_LAG = () => 0;
4
4
  /**
5
5
  * Event-loop-lag sampler. By default it measures timer drift using only Web/JS runtime primitives, so
@@ -159,7 +159,7 @@ export function createAdmissionController(options) {
159
159
  return inFlight < maxInFlight + reserved ? occupy() : shedResponse("policy");
160
160
  }
161
161
  }
162
- // Loop-lag shed: the box is behind on the event loop — reject regardless of slot count.
162
+ // Loop-lag shed: the box is behind on the event loop - reject regardless of slot count.
163
163
  if (lag > maxLagMs)
164
164
  return shedResponse("loop-lag");
165
165
  // FAST PATH (O(1)): a slot is free and lag is fine. No timer, no queue, no extra await.
@@ -8,7 +8,7 @@ export interface CacheControlOptions {
8
8
  }
9
9
  /**
10
10
  * Set a `Cache-Control` header on matching responses. `value` is either a fixed directive string or a
11
- * function of the request (return `undefined` to leave a response untouched — e.g. cache by path).
11
+ * function of the request (return `undefined` to leave a response untouched - e.g. cache by path).
12
12
  * Defaults to `GET`/`HEAD` + 2xx, and never clobbers a `Cache-Control` the handler set itself.
13
13
  *
14
14
  * ```ts
@@ -1,7 +1,7 @@
1
1
  import { definePlugin } from "@nifrajs/core/server";
2
2
  /**
3
3
  * Set a `Cache-Control` header on matching responses. `value` is either a fixed directive string or a
4
- * function of the request (return `undefined` to leave a response untouched — e.g. cache by path).
4
+ * function of the request (return `undefined` to leave a response untouched - e.g. cache by path).
5
5
  * Defaults to `GET`/`HEAD` + 2xx, and never clobbers a `Cache-Control` the handler set itself.
6
6
  *
7
7
  * ```ts
package/dist/cache.js CHANGED
@@ -189,7 +189,7 @@ export function cache(options) {
189
189
  return withStatusHeader(res, cacheStatusHeader, "BYPASS");
190
190
  }
191
191
  // Shared-cache safety (RFC 9111 §3.5): never store a response to an authenticated request
192
- // (Authorization or Cookie) unless it's explicitly public — otherwise a personalized 200 leaks
192
+ // (Authorization or Cookie) unless it's explicitly public - otherwise a personalized 200 leaks
193
193
  // across users. Opt in with `cacheAuthenticated: true` for a route that's the same for everyone.
194
194
  if (!cacheAuthenticated &&
195
195
  (req.headers.has("authorization") || req.headers.has("cookie")) &&
@@ -14,7 +14,7 @@ export interface CompressionOptions {
14
14
  *
15
15
  * The body is **peeked** up to `threshold` bytes to enforce the size floor: a response that ends below
16
16
  * the threshold is sent uncompressed (gzip's ~20-byte overhead would enlarge it), and only larger
17
- * bodies are compressed — the buffered prefix is replayed and the remainder streamed. (Runtimes rarely
17
+ * bodies are compressed - the buffered prefix is replayed and the remainder streamed. (Runtimes rarely
18
18
  * set `Content-Length` on a constructed `Response`, so the header alone can't gate this.)
19
19
  *
20
20
  * Skips: clients that don't accept gzip, already-encoded responses, bodyless responses (204/304/HEAD),
@@ -30,7 +30,7 @@ const gzipHeaders = (source) => {
30
30
  *
31
31
  * The body is **peeked** up to `threshold` bytes to enforce the size floor: a response that ends below
32
32
  * the threshold is sent uncompressed (gzip's ~20-byte overhead would enlarge it), and only larger
33
- * bodies are compressed — the buffered prefix is replayed and the remainder streamed. (Runtimes rarely
33
+ * bodies are compressed - the buffered prefix is replayed and the remainder streamed. (Runtimes rarely
34
34
  * set `Content-Length` on a constructed `Response`, so the header alone can't gate this.)
35
35
  *
36
36
  * Skips: clients that don't accept gzip, already-encoded responses, bodyless responses (204/304/HEAD),
@@ -47,7 +47,7 @@ export function compression(options = {}) {
47
47
  return definePlugin("compression", (app) => app.onResponse(async (res, req) => {
48
48
  const body = res.body;
49
49
  if (body === null)
50
- return res; // 204/304/HEAD — nothing to compress
50
+ return res; // 204/304/HEAD - nothing to compress
51
51
  if (!(req.headers.get("accept-encoding") ?? "").toLowerCase().includes("gzip"))
52
52
  return res;
53
53
  if (res.headers.has("content-encoding"))
@@ -68,7 +68,7 @@ export function compression(options = {}) {
68
68
  while (total < threshold) {
69
69
  const { done, value } = await reader.read();
70
70
  if (done) {
71
- // Body ended below the threshold — too small to be worth gzipping. Emit as-is.
71
+ // Body ended below the threshold - too small to be worth gzipping. Emit as-is.
72
72
  return new Response(concat(buffered, total), {
73
73
  status: res.status,
74
74
  statusText: res.statusText,
@@ -78,7 +78,7 @@ export function compression(options = {}) {
78
78
  buffered.push(value);
79
79
  total += value.byteLength;
80
80
  }
81
- // Over the threshold: replay the buffered prefix, then stream the rest — all through gzip.
81
+ // Over the threshold: replay the buffered prefix, then stream the rest - all through gzip.
82
82
  // (No explicit generic: it lets the chunk type infer from the non-shared reader so the stream
83
83
  // matches `CompressionStream`'s `Uint8Array` typing.)
84
84
  const source = new ReadableStream({
package/dist/cors.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface CorsOptions {
18
18
  * short-circuits to `204` via `onRequest`; the origin/credentials headers are added in
19
19
  * `onResponse`, so they also land on errors, 404s, and the preflight itself.
20
20
  *
21
- * Throws at construction if `credentials: true` is paired with `origin: "*"` — the
21
+ * Throws at construction if `credentials: true` is paired with `origin: "*"` - the
22
22
  * browser rejects that combination, so we fail loud instead of shipping dead CORS.
23
23
  */
24
24
  export declare function cors(options?: CorsOptions): Middleware;
package/dist/cors.js CHANGED
@@ -25,14 +25,14 @@ function resolveAllowOrigin(option, requestOrigin) {
25
25
  * short-circuits to `204` via `onRequest`; the origin/credentials headers are added in
26
26
  * `onResponse`, so they also land on errors, 404s, and the preflight itself.
27
27
  *
28
- * Throws at construction if `credentials: true` is paired with `origin: "*"` — the
28
+ * Throws at construction if `credentials: true` is paired with `origin: "*"` - the
29
29
  * browser rejects that combination, so we fail loud instead of shipping dead CORS.
30
30
  */
31
31
  export function cors(options = {}) {
32
32
  const origin = options.origin ?? "*";
33
33
  const credentials = options.credentials ?? false;
34
34
  if (credentials && origin === "*") {
35
- throw new Error('cors: `credentials: true` cannot be combined with `origin: "*"` — list explicit origin(s).');
35
+ throw new Error('cors: `credentials: true` cannot be combined with `origin: "*"` - list explicit origin(s).');
36
36
  }
37
37
  const methods = (options.methods ?? DEFAULT_METHODS).join(", ");
38
38
  const allowedHeaders = options.allowedHeaders?.join(", ");
@@ -0,0 +1,34 @@
1
+ import { type CapabilityExecutionJournal } from "@nifrajs/core/capabilities";
2
+ import { type NifraPlugin } from "@nifrajs/core/server";
3
+ export interface DurableCommandOptions {
4
+ /** Where effect transitions are recorded. `createDurableEffectJournal` from
5
+ * `@nifrajs/core/durable-execution` is the one this is built for; any implementation of the seam
6
+ * works, and a memory-backed one is a test double, not a durable command. */
7
+ readonly journal: CapabilityExecutionJournal;
8
+ }
9
+ /**
10
+ * Journal every capability effect on the routes below it, and declare the evidence that says so.
11
+ *
12
+ * A capability defined `idempotency: "durable"` requires `nifra.durable-command` evidence, and until
13
+ * this adapter existed nothing in the framework produced it. The only way to satisfy the tier was to
14
+ * write `assurance: ["nifra.durable-command"]` on the route - an assertion with nothing behind it,
15
+ * wrong in both directions: a route that genuinely journals its effects but omits the string fails
16
+ * `nifra check`, and a route that journals nothing but includes it passes.
17
+ *
18
+ * The evidence is a by-product here rather than a claim. Installing the adapter puts the journal on
19
+ * the request, so `executeCapability` records intent before an effect runs and exactly one terminal
20
+ * outcome after - which is what the tier is asking about. A call that passes its own `journal` still
21
+ * uses that one.
22
+ *
23
+ * const commands = durableCommand({ journal: createDurableEffectJournal({ store }) })
24
+ * const app = server().use(commands).post("/charge", { capabilities: ["billing.charge"] }, …)
25
+ *
26
+ * `beforeHandle` is order-scoped, like the auth plugins: routes registered before `.use(...)` are not
27
+ * covered, and the `subsequent` scope on the evidence says the same thing to `nifra check`.
28
+ *
29
+ * Note what this does NOT claim. Response replay (`schema.idempotency`) is a different guarantee and
30
+ * deliberately never proves this one: replaying a stored response after a crash mid-effect does not
31
+ * make the effect exactly-once, and the journal is what survives the crash.
32
+ */
33
+ export declare function durableCommand(options: DurableCommandOptions): NifraPlugin;
34
+ //# sourceMappingURL=durable-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-command.d.ts","sourceRoot":"","sources":["../src/durable-command.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAErE,MAAM,WAAW,qBAAqB;IACpC;;iFAE6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAA;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,WAAW,CAuB1E"}
@@ -0,0 +1,50 @@
1
+ import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance";
2
+ import { attachCapabilityJournal, } from "@nifrajs/core/capabilities";
3
+ import { definePlugin } from "@nifrajs/core/server";
4
+ /**
5
+ * Journal every capability effect on the routes below it, and declare the evidence that says so.
6
+ *
7
+ * A capability defined `idempotency: "durable"` requires `nifra.durable-command` evidence, and until
8
+ * this adapter existed nothing in the framework produced it. The only way to satisfy the tier was to
9
+ * write `assurance: ["nifra.durable-command"]` on the route - an assertion with nothing behind it,
10
+ * wrong in both directions: a route that genuinely journals its effects but omits the string fails
11
+ * `nifra check`, and a route that journals nothing but includes it passes.
12
+ *
13
+ * The evidence is a by-product here rather than a claim. Installing the adapter puts the journal on
14
+ * the request, so `executeCapability` records intent before an effect runs and exactly one terminal
15
+ * outcome after - which is what the tier is asking about. A call that passes its own `journal` still
16
+ * uses that one.
17
+ *
18
+ * const commands = durableCommand({ journal: createDurableEffectJournal({ store }) })
19
+ * const app = server().use(commands).post("/charge", { capabilities: ["billing.charge"] }, …)
20
+ *
21
+ * `beforeHandle` is order-scoped, like the auth plugins: routes registered before `.use(...)` are not
22
+ * covered, and the `subsequent` scope on the evidence says the same thing to `nifra check`.
23
+ *
24
+ * Note what this does NOT claim. Response replay (`schema.idempotency`) is a different guarantee and
25
+ * deliberately never proves this one: replaying a stored response after a crash mid-effect does not
26
+ * make the effect exactly-once, and the journal is what survives the crash.
27
+ */
28
+ export function durableCommand(options) {
29
+ const journal = options.journal;
30
+ if (journal === null || typeof journal !== "object") {
31
+ throw new TypeError("durableCommand: journal must be a CapabilityExecutionJournal");
32
+ }
33
+ for (const method of ["intent", "executing", "committed", "failed"]) {
34
+ if (typeof journal[method] !== "function") {
35
+ // Fail at wiring time. A journal missing a transition would otherwise surface as a mid-effect
36
+ // TypeError on the first write in production, with the evidence already declared.
37
+ throw new TypeError(`durableCommand: journal.${method} must be a function`);
38
+ }
39
+ }
40
+ const plugin = definePlugin("nifra:durable-command", (app) => app.beforeHandle((c) => {
41
+ attachCapabilityJournal(c, journal);
42
+ return undefined;
43
+ }));
44
+ return withRouteAssurance(plugin, {
45
+ id: NIFRA_ASSURANCE.DURABLE_COMMAND,
46
+ source: "durable-command",
47
+ scope: "subsequent",
48
+ });
49
+ }
50
+ //# sourceMappingURL=durable-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-command.js","sourceRoot":"","sources":["../src/durable-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EACL,uBAAuB,GAExB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,YAAY,EAAoB,MAAM,sBAAsB,CAAA;AASrE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;IAC/B,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,IAAI,SAAS,CAAC,8DAA8D,CAAC,CAAA;IACrF,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAU,EAAE,CAAC;QAC7E,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC;YAC1C,8FAA8F;YAC9F,kFAAkF;YAClF,MAAM,IAAI,SAAS,CAAC,2BAA2B,MAAM,qBAAqB,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,uBAAuB,EAAE,CAAC,GAAG,EAAE,EAAE,CAC3D,GAAG,CAAC,YAAY,CAAC,CAAC,CAAS,EAAE,EAAE;QAC7B,uBAAuB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;QACnC,OAAO,SAAS,CAAA;IAClB,CAAC,CAAC,CACH,CAAA;IACD,OAAO,kBAAkB,CAAC,MAAM,EAAE;QAChC,EAAE,EAAE,eAAe,CAAC,eAAe;QACnC,MAAM,EAAE,iBAAiB;QACzB,KAAK,EAAE,YAAY;KACpB,CAAC,CAAA;AACJ,CAAC"}
package/dist/etag.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export interface ETagOptions {
2
- /** Emit a weak validator (`W/"…"`). Default `true` — it's a content hash, not a byte-for-byte promise. */
2
+ /** Emit a weak validator (`W/"…"`). Default `true` - it's a content hash, not a byte-for-byte promise. */
3
3
  readonly weak?: boolean;
4
4
  /** Maximum response bytes to hash. Default `1_000_000`; larger responses pass through unchanged. */
5
5
  readonly maxBytes?: number;
6
6
  }
7
7
  /**
8
8
  * A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
9
- * **`304 Not Modified`** when the client's `If-None-Match` matches — saving bandwidth on unchanged
9
+ * **`304 Not Modified`** when the client's `If-None-Match` matches - saving bandwidth on unchanged
10
10
  * responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
11
11
  * Idempotent.
12
12
  */
package/dist/etag.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { definePlugin } from "@nifrajs/core/server";
2
- /** 32-bit FNV-1a over bytes → hex. A fast, dependency-free content fingerprint for ETags — not crypto. */
2
+ /** 32-bit FNV-1a over bytes → hex. A fast, dependency-free content fingerprint for ETags - not crypto. */
3
3
  function fnv1a(bytes) {
4
4
  let h = 0x811c9dc5;
5
5
  for (let i = 0; i < bytes.length; i++) {
@@ -10,7 +10,7 @@ function fnv1a(bytes) {
10
10
  }
11
11
  /**
12
12
  * A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
13
- * **`304 Not Modified`** when the client's `If-None-Match` matches — saving bandwidth on unchanged
13
+ * **`304 Not Modified`** when the client's `If-None-Match` matches - saving bandwidth on unchanged
14
14
  * responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
15
15
  * Idempotent.
16
16
  */
@@ -33,7 +33,7 @@ export function etag(options = {}) {
33
33
  const headers = new Headers(res.headers);
34
34
  headers.set("ETag", tag);
35
35
  if (matchesIfNoneMatch(req.headers.get("if-none-match"), tag)) {
36
- // A 304 carries no body — drop the body-describing headers so strict intermediaries don't see a
36
+ // A 304 carries no body - drop the body-describing headers so strict intermediaries don't see a
37
37
  // null body with a non-zero Content-Length.
38
38
  headers.delete("content-length");
39
39
  headers.delete("content-type");
@@ -1,8 +1,8 @@
1
1
  type MaybePromise<T> = T | Promise<T>;
2
2
  export interface HealthcheckOptions {
3
- /** Liveness path — always `200` while the process is up. Default `"/health"`. */
3
+ /** Liveness path - always `200` while the process is up. Default `"/health"`. */
4
4
  readonly path?: string;
5
- /** Readiness path — runs `checks`; `200` if all pass, `503` otherwise. Default `"/ready"`. */
5
+ /** Readiness path - runs `checks`; `200` if all pass, `503` otherwise. Default `"/ready"`. */
6
6
  readyPath?: string;
7
7
  /** Readiness checks (DB ping, cache reachable, …). Each returns a boolean or throws; a throw counts
8
8
  * as failing. `/ready` reports each by name and is `200` only when all pass. */
@@ -22,7 +22,7 @@ export function healthcheck(options = {}) {
22
22
  app.register("GET", readyPath, undefined, async () => {
23
23
  const results = {};
24
24
  let ready = true;
25
- // Run checks concurrently; a throw (or false) marks that check — and the whole probe — not ready.
25
+ // Run checks concurrently; a throw (or false) marks that check - and the whole probe - not ready.
26
26
  await Promise.all(checkNames.map(async (name) => {
27
27
  try {
28
28
  results[name] = (await checks[name]?.()) === true;
@@ -1,18 +1,18 @@
1
1
  import { type Middleware } from "@nifrajs/core/server";
2
2
  /**
3
- * Idempotency keys for unsafe requests — a client retrying a `POST` (dropped connection, impatient
3
+ * Idempotency keys for unsafe requests - a client retrying a `POST` (dropped connection, impatient
4
4
  * tap) with the same `Idempotency-Key` gets the **first** response replayed instead of the side effect
5
5
  * running twice (double-charge, double-publish). Runs in `onRequest` (before the handler), so a replay
6
6
  * or an in-flight collision short-circuits before any mutation.
7
7
  *
8
- * Pair it with a DB uniqueness constraint — this stops the *retry*, the constraint is the source of
8
+ * Pair it with a DB uniqueness constraint - this stops the *retry*, the constraint is the source of
9
9
  * truth for genuinely-concurrent distinct requests. Production MUST use a shared {@link IdempotencyStore}
10
10
  * (Redis, etc.) so the guarantee holds across instances; {@link MemoryIdempotencyStore} is dev-only.
11
11
  */
12
12
  /** A captured response, replayed verbatim on a retry. Body is base64 (binary-safe + JSON-serializable). */
13
13
  export interface IdempotencyRecord {
14
14
  readonly status: number;
15
- /** Response headers, **excluding `Set-Cookie`** (cookies are session-specific — see {@link idempotency}). */
15
+ /** Response headers, **excluding `Set-Cookie`** (cookies are session-specific - see {@link idempotency}). */
16
16
  readonly headers: ReadonlyArray<readonly [string, string]>;
17
17
  /** Response body, base64-encoded. */
18
18
  readonly body: string;
@@ -43,7 +43,7 @@ export interface IdempotencyStore {
43
43
  release(key: string): Promise<void>;
44
44
  }
45
45
  export interface MemoryIdempotencyStoreOptions {
46
- /** Allow the in-memory store in production. Off by default — a per-instance store can't dedupe across instances. */
46
+ /** Allow the in-memory store in production. Off by default - a per-instance store can't dedupe across instances. */
47
47
  readonly allowInProduction?: boolean;
48
48
  }
49
49
  /** In-process store. Refuses to run in production unless explicitly allowed (per-instance ⇒ no cross-instance dedupe). */
@@ -146,7 +146,7 @@ function replay(record) {
146
146
  /**
147
147
  * Idempotency-key middleware. Apply with `app.use(idempotency({ store }))`.
148
148
  *
149
- * **`Set-Cookie` is intentionally not cached or replayed** — a cookie set on the first request is
149
+ * **`Set-Cookie` is intentionally not cached or replayed** - a cookie set on the first request is
150
150
  * session-specific, so replaying it to a different caller (key collision or abuse) would leak/fixate a
151
151
  * session. Cache the body + status + the rest of the headers; let auth cookies re-issue per request.
152
152
  *
@@ -180,7 +180,7 @@ export function idempotency(options) {
180
180
  return undefined;
181
181
  const key = keyOf(req, header);
182
182
  if (key === null || key === "")
183
- return undefined; // opt-in per request — no key ⇒ no dedupe
183
+ return undefined; // opt-in per request - no key ⇒ no dedupe
184
184
  const claim = await store.begin(key, lockTtlMs);
185
185
  if (claim.state === "replay")
186
186
  return replay(claim.record);
@@ -215,7 +215,7 @@ export function idempotency(options) {
215
215
  throw err;
216
216
  }
217
217
  if (!("bytes" in captured)) {
218
- await store.release(key); // too large to store — return it, but don't cache
218
+ await store.release(key); // too large to store - return it, but don't cache
219
219
  return captured.response;
220
220
  }
221
221
  const bytes = captured.bytes;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * `@nifrajs/middleware` — composable, dependency-free middleware + plugins applied via `app.use()`.
2
+ * `@nifrajs/middleware` - composable, dependency-free middleware + plugins applied via `app.use()`.
3
3
  * The hardening set (`cors`, `rateLimit`, `securityHeaders`) returns a `@nifrajs/core` `Middleware`
4
4
  * (a hook bundle, context-agnostic). The seeded plugins (`requestId`, `logger`, `etag`, `bearer`,
5
- * `apiKey`) use the `definePlugin` convention — named (idempotent dedupe), and `requestId` threads
5
+ * `apiKey`) use the `definePlugin` convention - named (idempotent dedupe), and `requestId` threads
6
6
  * `c.requestId` into the handler context.
7
7
  */
8
8
  export { type AdmissionControllerHandle, type AdmissionEvidence, type AdmissionOptions, type AdmissionPolicy, type AdmissionSnapshot, createAdmissionController, createEventLoopLagSampler, type LoopDelayHistogram, type LoopDelayMonitor, type ShedReason, } from "./admission.js";
@@ -14,6 +14,7 @@ export { type Composable, combine, namedCombine } from "./combine.js";
14
14
  export { type CompressionOptions, compression } from "./compression.js";
15
15
  export { type CorsOptions, cors } from "./cors.js";
16
16
  export { type CsrfOptions, createCsrfToken, csrf, verifyCsrfToken, } from "./csrf.js";
17
+ export { type DurableCommandOptions, durableCommand } from "./durable-command.js";
17
18
  export { type ETagOptions, etag } from "./etag.js";
18
19
  export { type HealthcheckOptions, healthcheck } from "./healthcheck.js";
19
20
  export { type IdempotencyClaim, type IdempotencyOptions, type IdempotencyRecord, type IdempotencyStore, idempotency, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, } from "./idempotency.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * `@nifrajs/middleware` — composable, dependency-free middleware + plugins applied via `app.use()`.
2
+ * `@nifrajs/middleware` - composable, dependency-free middleware + plugins applied via `app.use()`.
3
3
  * The hardening set (`cors`, `rateLimit`, `securityHeaders`) returns a `@nifrajs/core` `Middleware`
4
4
  * (a hook bundle, context-agnostic). The seeded plugins (`requestId`, `logger`, `etag`, `bearer`,
5
- * `apiKey`) use the `definePlugin` convention — named (idempotent dedupe), and `requestId` threads
5
+ * `apiKey`) use the `definePlugin` convention - named (idempotent dedupe), and `requestId` threads
6
6
  * `c.requestId` into the handler context.
7
7
  */
8
8
  export { createAdmissionController, createEventLoopLagSampler, } from "./admission.js";
@@ -14,6 +14,7 @@ export { combine, namedCombine } from "./combine.js";
14
14
  export { compression } from "./compression.js";
15
15
  export { cors } from "./cors.js";
16
16
  export { createCsrfToken, csrf, verifyCsrfToken, } from "./csrf.js";
17
+ export { durableCommand } from "./durable-command.js";
17
18
  export { etag } from "./etag.js";
18
19
  export { healthcheck } from "./healthcheck.js";
19
20
  export { idempotency, MemoryIdempotencyStore, } from "./idempotency.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAML,yBAAyB,EACzB,yBAAyB,GAI1B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAIL,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAGL,KAAK,EACL,mBAAmB,EAGnB,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAA4B,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAmB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EAA2B,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAEL,eAAe,EACf,IAAI,EACJ,eAAe,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAA2B,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAKL,WAAW,EACX,sBAAsB,GAEvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA6C,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC9F,OAAO,EAUL,GAAG,EACH,IAAI,EACJ,GAAG,EACH,YAAY,EAIZ,SAAS,GACV,MAAM,UAAU,CAAA;AACjB,OAAO,EAGL,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAA;AACtB,OAAO,EAAsB,MAAM,EAAyB,MAAM,aAAa,CAAA;AAC/E,OAAO,EAA8B,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACjF,OAAO,EACL,oBAAoB,EAMpB,OAAO,GAGR,MAAM,cAAc,CAAA;AACrB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAA0B,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrE,OAAO,EACL,WAAW,EAKX,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAA+B,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACpF,OAAO,EAA8D,MAAM,EAAE,MAAM,aAAa,CAAA;AAChG,OAAO,EAIL,MAAM,EAEN,MAAM,GACP,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,mBAAmB,EAEnB,iBAAiB,GAClB,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAML,yBAAyB,EACzB,yBAAyB,GAI1B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAIL,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAGL,KAAK,EACL,mBAAmB,EAGnB,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAA4B,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAmB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACrE,OAAO,EAA2B,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAEL,eAAe,EACf,IAAI,EACJ,eAAe,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EAA8B,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACjF,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAA2B,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAKL,WAAW,EACX,sBAAsB,GAEvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA6C,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC9F,OAAO,EAUL,GAAG,EACH,IAAI,EACJ,GAAG,EACH,YAAY,EAIZ,SAAS,GACV,MAAM,UAAU,CAAA;AACjB,OAAO,EAGL,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAA;AACtB,OAAO,EAAsB,MAAM,EAAyB,MAAM,aAAa,CAAA;AAC/E,OAAO,EAA8B,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACjF,OAAO,EACL,oBAAoB,EAMpB,OAAO,GAGR,MAAM,cAAc,CAAA;AACrB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAA0B,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrE,OAAO,EACL,WAAW,EAKX,SAAS,GACV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAyB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAA+B,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACpF,OAAO,EAA8D,MAAM,EAAE,MAAM,aAAa,CAAA;AAChG,OAAO,EAIL,MAAM,EAEN,MAAM,GACP,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,mBAAmB,EAEnB,iBAAiB,GAClB,MAAM,qBAAqB,CAAA"}
package/dist/logger.d.ts CHANGED
@@ -11,8 +11,8 @@ export interface LoggerOptions {
11
11
  readonly log?: (fields: RequestLogFields) => void;
12
12
  }
13
13
  /**
14
- * A {@link definePlugin} plugin that logs one structured line per request — method, path, status,
15
- * and duration — via `onRequest`/`onResponse` (so it covers 404s and errors too). The start time is
14
+ * A {@link definePlugin} plugin that logs one structured line per request - method, path, status,
15
+ * and duration - via `onRequest`/`onResponse` (so it covers 404s and errors too). The start time is
16
16
  * paired to the request through a `WeakMap` (no per-request allocation leak). Idempotent.
17
17
  */
18
18
  export declare function logger(options?: LoggerOptions): import("@nifrajs/core/server").NifraPlugin<import("@nifrajs/core/server").AnyServer, import("@nifrajs/core/server").AnyServer>;
package/dist/logger.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { definePlugin } from "@nifrajs/core/server";
2
2
  /**
3
- * A {@link definePlugin} plugin that logs one structured line per request — method, path, status,
4
- * and duration — via `onRequest`/`onResponse` (so it covers 404s and errors too). The start time is
3
+ * A {@link definePlugin} plugin that logs one structured line per request - method, path, status,
4
+ * and duration - via `onRequest`/`onResponse` (so it covers 404s and errors too). The start time is
5
5
  * paired to the request through a `WeakMap` (no per-request allocation leak). Idempotent.
6
6
  */
7
7
  export function logger(options = {}) {
package/dist/openapi.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** A registered route as seen by {@link buildOpenApiDocument} — structurally a `@nifrajs/core`
1
+ /** A registered route as seen by {@link buildOpenApiDocument} - structurally a `@nifrajs/core`
2
2
  * `RouteDescriptor` (so `app.routes()` is passed straight through). */
3
3
  export interface RouteLike {
4
4
  readonly method: string;
@@ -49,7 +49,7 @@ export interface OpenApiOptions {
49
49
  readonly method: string;
50
50
  readonly path: string;
51
51
  }) => boolean;
52
- /** Per-operation overrides keyed by `"GET /users/:id"`, shallow-merged over the generated skeleton —
52
+ /** Per-operation overrides keyed by `"GET /users/:id"`, shallow-merged over the generated skeleton -
53
53
  * the escape hatch for rich request/response schemas, tags, and per-op security (Standard Schema
54
54
  * can't be introspected). */
55
55
  readonly operations?: Readonly<Record<string, Record<string, unknown>>>;
@@ -66,10 +66,10 @@ export interface OpenApiOptions {
66
66
  */
67
67
  export declare function buildOpenApiDocument(routes: readonly RouteLike[], options?: OpenApiOptions): Record<string, unknown>;
68
68
  /**
69
- * Serve an OpenAPI 3.1 document (a structural subset — see {@link buildOpenApiDocument}) at
69
+ * Serve an OpenAPI 3.1 document (a structural subset - see {@link buildOpenApiDocument}) at
70
70
  * `options.path` (default `/openapi.json`), generated from the app's registered routes. Generation is
71
71
  * **lazy + memoized**: it reads `app.routes()` on the first request, by which point every route is
72
- * registered — so the plugin's own position in the chain doesn't matter.
72
+ * registered - so the plugin's own position in the chain doesn't matter.
73
73
  *
74
74
  * Pass `ui: true` (or `ui: { path, title, cdn }`) to also serve a Scalar API-reference page (default
75
75
  * `/reference`) that renders the spec. The page loads Scalar from a CDN; pin/self-host via `ui.cdn`,