@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.
- package/README.md +22 -22
- package/dist/_utils.d.ts +3 -3
- package/dist/_utils.js +3 -3
- package/dist/admission.d.ts +2 -2
- package/dist/admission.js +2 -2
- package/dist/cache-control.d.ts +1 -1
- package/dist/cache-control.js +1 -1
- package/dist/cache.js +1 -1
- package/dist/compression.d.ts +1 -1
- package/dist/compression.js +4 -4
- package/dist/cors.d.ts +1 -1
- package/dist/cors.js +2 -2
- package/dist/durable-command.d.ts +34 -0
- package/dist/durable-command.d.ts.map +1 -0
- package/dist/durable-command.js +50 -0
- package/dist/durable-command.js.map +1 -0
- package/dist/etag.d.ts +2 -2
- package/dist/etag.js +3 -3
- package/dist/healthcheck.d.ts +2 -2
- package/dist/healthcheck.js +1 -1
- package/dist/idempotency.d.ts +4 -4
- package/dist/idempotency.js +3 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +2 -2
- package/dist/openapi.d.ts +4 -4
- package/dist/openapi.js +4 -4
- package/dist/rate-limit.d.ts +8 -8
- package/dist/rate-limit.js +5 -5
- package/dist/request-id.d.ts +1 -1
- package/dist/request-id.js +1 -1
- package/dist/security-headers.d.ts +1 -1
- package/dist/security-headers.js +1 -1
- package/dist/token-auth.d.ts +6 -6
- package/dist/token-auth.js +4 -4
- package/package.json +4 -4
- package/src/_utils.ts +3 -3
- package/src/admission.ts +4 -4
- package/src/cache-control.ts +1 -1
- package/src/cache.ts +1 -1
- package/src/compression.ts +4 -4
- package/src/cors.ts +2 -2
- package/src/durable-command.ts +62 -0
- package/src/etag.ts +4 -4
- package/src/healthcheck.ts +3 -3
- package/src/idempotency.ts +7 -7
- package/src/index.ts +3 -2
- package/src/logger.ts +2 -2
- package/src/openapi.ts +8 -8
- package/src/rate-limit.ts +10 -10
- package/src/request-id.ts +1 -1
- package/src/security-headers.ts +2 -2
- 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)
|
|
4
|
-
limits, auth, CSRF, JWT/JWKS, IP restriction, response caching, timing, and ops helpers
|
|
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 })`**
|
|
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)`**
|
|
39
|
+
- **`basicAuth(options)`** - Basic Auth plugin with constant-time static credential comparison or
|
|
40
40
|
a custom verifier.
|
|
41
|
-
- **`bearer(options)` / `apiKey(options)`**
|
|
42
|
-
- **`csrf({ secret })`**
|
|
43
|
-
- **`jwt(options)` + `verifyJwt()` / `tryVerifyJwt()` + `jwk()` / `jwks()`**
|
|
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)`**
|
|
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)`**
|
|
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)`**
|
|
51
|
+
- **`securityHeaders(options)`** - `X-Content-Type-Options`, `X-Frame-Options`,
|
|
52
52
|
`Referrer-Policy` by default; opt-in HSTS and CSP.
|
|
53
|
-
- **`rateLimit(options)`**
|
|
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)
|
|
58
|
-
- **`cache({ store, ttlMs })` / `responseCache(...)`**
|
|
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()`**
|
|
62
|
-
- **`prettyJson()`**
|
|
63
|
-
- **`methodOverride()`**
|
|
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()`**
|
|
66
|
-
- **`poweredBy()`**
|
|
67
|
-
- **`language()` / `pickLanguage()`**
|
|
68
|
-
- **`combine()` / `namedCombine()`**
|
|
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()`**
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 {
|
package/dist/admission.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/dist/cache-control.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cache-control.js
CHANGED
|
@@ -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
|
|
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
|
|
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")) &&
|
package/dist/compression.d.ts
CHANGED
|
@@ -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
|
|
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),
|
package/dist/compression.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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: "*"`
|
|
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: "*"`
|
|
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: "*"`
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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");
|
package/dist/healthcheck.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
type MaybePromise<T> = T | Promise<T>;
|
|
2
2
|
export interface HealthcheckOptions {
|
|
3
|
-
/** Liveness path
|
|
3
|
+
/** Liveness path - always `200` while the process is up. Default `"/health"`. */
|
|
4
4
|
readonly path?: string;
|
|
5
|
-
/** Readiness path
|
|
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. */
|
package/dist/healthcheck.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/idempotency.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { type Middleware } from "@nifrajs/core/server";
|
|
2
2
|
/**
|
|
3
|
-
* Idempotency keys for unsafe requests
|
|
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
|
|
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
|
|
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
|
|
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). */
|
package/dist/idempotency.js
CHANGED
|
@@ -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**
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
15
|
-
* and duration
|
|
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
|
|
4
|
-
* and duration
|
|
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}
|
|
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
|
|
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
|
|
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`,
|