@nifrajs/middleware 2.10.0 → 2.12.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/dist/_utils.d.ts +14 -2
- package/dist/_utils.d.ts.map +1 -1
- package/dist/_utils.js +25 -10
- package/dist/_utils.js.map +1 -1
- package/dist/basic-auth.d.ts +2 -2
- package/dist/basic-auth.d.ts.map +1 -1
- package/dist/basic-auth.js +2 -2
- package/dist/basic-auth.js.map +1 -1
- package/dist/cache-control.d.ts +1 -1
- package/dist/cache-control.d.ts.map +1 -1
- package/dist/cache-control.js +2 -2
- package/dist/cache-control.js.map +1 -1
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +13 -1
- package/dist/cache.js.map +1 -1
- package/dist/combine.d.ts +11 -5
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +11 -4
- package/dist/combine.js.map +1 -1
- package/dist/compression.d.ts +8 -1
- package/dist/compression.d.ts.map +1 -1
- package/dist/compression.js +9 -2
- package/dist/compression.js.map +1 -1
- package/dist/csrf.d.ts +11 -4
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +19 -5
- package/dist/csrf.js.map +1 -1
- package/dist/durable-command.d.ts +2 -2
- package/dist/durable-command.d.ts.map +1 -1
- package/dist/durable-command.js +2 -2
- package/dist/durable-command.js.map +1 -1
- package/dist/etag.d.ts +2 -2
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +3 -3
- package/dist/etag.js.map +1 -1
- package/dist/healthcheck.d.ts +2 -1
- package/dist/healthcheck.d.ts.map +1 -1
- package/dist/healthcheck.js +5 -2
- package/dist/healthcheck.js.map +1 -1
- package/dist/idempotency.d.ts +51 -5
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +110 -13
- package/dist/idempotency.js.map +1 -1
- package/dist/jwt.d.ts +2 -2
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +14 -3
- package/dist/jwt.js.map +1 -1
- package/dist/language.d.ts +9 -1
- package/dist/language.d.ts.map +1 -1
- package/dist/language.js +8 -2
- package/dist/language.js.map +1 -1
- package/dist/logger.d.ts +9 -4
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +9 -5
- package/dist/logger.js.map +1 -1
- package/dist/method-override.d.ts +1 -1
- package/dist/method-override.d.ts.map +1 -1
- package/dist/method-override.js +2 -2
- package/dist/method-override.js.map +1 -1
- package/dist/openapi.d.ts +2 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +4 -2
- package/dist/openapi.js.map +1 -1
- package/dist/pretty-json.d.ts +1 -1
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +2 -2
- package/dist/pretty-json.js.map +1 -1
- package/dist/problem-details.d.ts +1 -1
- package/dist/problem-details.d.ts.map +1 -1
- package/dist/problem-details.js +2 -2
- package/dist/problem-details.js.map +1 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +6 -8
- package/dist/rate-limit.js.map +1 -1
- package/dist/request-id.d.ts +12 -4
- package/dist/request-id.d.ts.map +1 -1
- package/dist/request-id.js +62 -7
- package/dist/request-id.js.map +1 -1
- package/dist/security-headers.d.ts +11 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +12 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/timing.d.ts +4 -1
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +2 -2
- package/dist/timing.js.map +1 -1
- package/dist/token-auth.d.ts +2 -2
- package/dist/token-auth.d.ts.map +1 -1
- package/dist/token-auth.js +2 -2
- package/dist/token-auth.js.map +1 -1
- package/dist/trailing-slash.d.ts +2 -2
- package/dist/trailing-slash.d.ts.map +1 -1
- package/dist/trailing-slash.js +3 -3
- package/dist/trailing-slash.js.map +1 -1
- package/package.json +3 -3
- package/src/_utils.ts +27 -10
- package/src/basic-auth.ts +3 -3
- package/src/cache-control.ts +2 -2
- package/src/cache.ts +20 -1
- package/src/combine.ts +22 -10
- package/src/compression.ts +9 -2
- package/src/csrf.ts +31 -15
- package/src/durable-command.ts +3 -3
- package/src/etag.ts +3 -3
- package/src/healthcheck.ts +6 -3
- package/src/idempotency.ts +146 -18
- package/src/jwt.ts +22 -4
- package/src/language.ts +26 -16
- package/src/logger.ts +10 -6
- package/src/method-override.ts +2 -2
- package/src/openapi.ts +5 -3
- package/src/pretty-json.ts +2 -2
- package/src/problem-details.ts +2 -2
- package/src/rate-limit.ts +6 -8
- package/src/request-id.ts +63 -11
- package/src/security-headers.ts +23 -0
- package/src/timing.ts +3 -3
- package/src/token-auth.ts +3 -3
- package/src/trailing-slash.ts +3 -3
package/src/request-id.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ContextPlugin, defineContextPlugin } from "@nifrajs/core/server"
|
|
2
|
+
import { setNodeHeader, withHeaders } from "./_utils.ts"
|
|
2
3
|
|
|
3
4
|
export interface RequestIdOptions {
|
|
4
5
|
/** Header read for an inbound id (trace propagation) + echoed on the response. Default `"x-request-id"`. */
|
|
@@ -8,23 +9,74 @@ export interface RequestIdOptions {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* A {@link
|
|
12
|
+
* A {@link defineContextPlugin} plugin that gives every request a stable id: it reuses an inbound
|
|
12
13
|
* `x-request-id` (or generates one), exposes it on the handler context as **`c.requestId`** (typed,
|
|
13
|
-
* threaded by `derive`), and echoes it on
|
|
14
|
-
*
|
|
14
|
+
* threaded by `derive`), and echoes it on EVERY response leaving the app - handler-returned raw
|
|
15
|
+
* `Response`s and redirects, `onRequest` short-circuits (an auth 401, an admission 429), and
|
|
16
|
+
* framework-generated 404/405/422/timeout responses included. Core deliberately does not merge
|
|
17
|
+
* `c.set.headers` onto a handler-returned `Response`, so the echo lives in an app-global
|
|
18
|
+
* `onResponse` hook (paired with the derive through a `WeakMap` keyed on the request) with a node
|
|
19
|
+
* twin, keeping the Node adapter on its direct writer with no request-walk cost. Idempotent -
|
|
20
|
+
* applying it twice is a no-op.
|
|
15
21
|
*
|
|
16
22
|
* ```ts
|
|
17
23
|
* app.use(requestId()) // c.requestId available downstream; X-Request-Id on responses
|
|
18
24
|
* ```
|
|
19
25
|
*/
|
|
20
|
-
export function requestId(options: RequestIdOptions = {}) {
|
|
26
|
+
export function requestId(options: RequestIdOptions = {}): ContextPlugin<{ requestId: string }> {
|
|
21
27
|
const header = options.header ?? "x-request-id"
|
|
28
|
+
// The wire spelling: `c.set.headers` and the Node outcome record both store lowercase names, so
|
|
29
|
+
// the response twin's "already echoed?" probe and the derive's write must agree on it.
|
|
30
|
+
const wireHeader = header.toLowerCase()
|
|
22
31
|
const generate = options.generate ?? (() => crypto.randomUUID())
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// Web-lane derive-to-response pairing for GENERATED ids (an inbound one is re-readable from the
|
|
33
|
+
// request in onResponse). Entries are deleted in onResponse; a Node-lane derive also writes here
|
|
34
|
+
// but its id travels via the `c.set` write into the outcome record, and the weakly-held entry is
|
|
35
|
+
// simply collected with the request.
|
|
36
|
+
const ids = new WeakMap<Request, string>()
|
|
37
|
+
// defineContextPlugin, not definePlugin: the latter would infer `app` as `AnyServer` here and make
|
|
38
|
+
// `.use(requestId())` return `Server<any, any>`, erasing the route registry for every route the app
|
|
39
|
+
// declares afterwards - and with it the end-to-end types of any typed client built from the app.
|
|
40
|
+
return defineContextPlugin<{ requestId: string }>("requestId", (app) =>
|
|
41
|
+
app
|
|
42
|
+
// No `name` on the bundle: the surrounding plugin's "requestId" dedupe already makes the
|
|
43
|
+
// install idempotent, and a bundle named the same would be skipped as already-installed.
|
|
44
|
+
// Response hooks ONLY - no onRequest, so the bundle adds nothing to the request walk and the
|
|
45
|
+
// Node adapter's native request lane stays engaged without needing a no-op twin.
|
|
46
|
+
.use({
|
|
47
|
+
onResponse(res, req) {
|
|
48
|
+
const id = ids.get(req)
|
|
49
|
+
ids.delete(req)
|
|
50
|
+
// Already carried (the derive's `c.set` write merged into a framework-rendered response,
|
|
51
|
+
// or the handler set it explicitly): leave the response untouched.
|
|
52
|
+
if (res.headers.get(header) !== null) return res
|
|
53
|
+
// The id the derive generated; else the inbound header, then a fresh id - so a response
|
|
54
|
+
// no derive saw (a 404, an earlier middleware's short-circuit) is still covered.
|
|
55
|
+
const echo = id ?? req.headers.get(header) ?? generate()
|
|
56
|
+
return withHeaders(res, (headers) => {
|
|
57
|
+
headers.set(header, echo)
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
onNodeResponse(res, req) {
|
|
61
|
+
// The common node path (a handler route, whose derive wrote the id through `c.set`) pays
|
|
62
|
+
// exactly this one record-property check. Anything the derive never saw - a 404, a native
|
|
63
|
+
// short-circuit - gets the inbound id or a fresh one here.
|
|
64
|
+
if (res.headers?.[wireHeader] !== undefined) return
|
|
65
|
+
setNodeHeader(res, wireHeader, req.header(header) ?? generate())
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
.derive((c) => {
|
|
69
|
+
// Inbound header first (readable on every lane without materializing a Request); generate
|
|
70
|
+
// otherwise, ONCE. A generated id is stored keyed on `c.req` so the Web onResponse hook
|
|
71
|
+
// echoes the exact id a raw-Response handler saw as `c.requestId`; the `c.set` write below
|
|
72
|
+
// carries it into a framework-rendered result on every lane - on Node-direct, into the
|
|
73
|
+
// outcome record, where the response twin sees it as already echoed. Header and
|
|
74
|
+
// `c.requestId` are therefore the same value everywhere with a single generation.
|
|
75
|
+
const inbound = c.header(header)
|
|
76
|
+
const id = inbound ?? generate()
|
|
77
|
+
if (inbound === null) ids.set(c.req, id)
|
|
78
|
+
c.set.headers[wireHeader] = id
|
|
79
|
+
return { requestId: id }
|
|
80
|
+
}),
|
|
29
81
|
)
|
|
30
82
|
}
|
package/src/security-headers.ts
CHANGED
|
@@ -14,6 +14,17 @@ export interface SecurityHeadersOptions {
|
|
|
14
14
|
readonly frameOptions?: "DENY" | "SAMEORIGIN"
|
|
15
15
|
/** `Referrer-Policy`. Default `"no-referrer"`. */
|
|
16
16
|
readonly referrerPolicy?: string
|
|
17
|
+
/** `Cross-Origin-Opener-Policy`. Off by default - isolates the browsing context group; opt in
|
|
18
|
+
* when the app tolerates losing `window.opener` links from cross-origin pages. */
|
|
19
|
+
readonly crossOriginOpenerPolicy?: "same-origin" | "same-origin-allow-popups" | "unsafe-none"
|
|
20
|
+
/** `Cross-Origin-Embedder-Policy`. Off by default - `require-corp` breaks embedding of
|
|
21
|
+
* cross-origin resources that do not opt in, so this is a deliberate decision. */
|
|
22
|
+
readonly crossOriginEmbedderPolicy?: "require-corp" | "credentialless" | "unsafe-none"
|
|
23
|
+
/** `Cross-Origin-Resource-Policy`. Off by default (app-specific: assets meant for other
|
|
24
|
+
* origins need `cross-origin`). */
|
|
25
|
+
readonly crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin"
|
|
26
|
+
/** `Permissions-Policy` value, e.g. `"camera=(), geolocation=()"`. Off by default (app-specific). */
|
|
27
|
+
readonly permissionsPolicy?: string
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -46,6 +57,18 @@ export function securityHeaders(options: SecurityHeadersOptions = {}): Middlewar
|
|
|
46
57
|
}
|
|
47
58
|
if (hstsValue !== undefined) declared["strict-transport-security"] = hstsValue
|
|
48
59
|
if (csp !== undefined) declared["content-security-policy"] = csp
|
|
60
|
+
if (options.crossOriginOpenerPolicy !== undefined) {
|
|
61
|
+
declared["cross-origin-opener-policy"] = options.crossOriginOpenerPolicy
|
|
62
|
+
}
|
|
63
|
+
if (options.crossOriginEmbedderPolicy !== undefined) {
|
|
64
|
+
declared["cross-origin-embedder-policy"] = options.crossOriginEmbedderPolicy
|
|
65
|
+
}
|
|
66
|
+
if (options.crossOriginResourcePolicy !== undefined) {
|
|
67
|
+
declared["cross-origin-resource-policy"] = options.crossOriginResourcePolicy
|
|
68
|
+
}
|
|
69
|
+
if (options.permissionsPolicy !== undefined) {
|
|
70
|
+
declared["permissions-policy"] = options.permissionsPolicy
|
|
71
|
+
}
|
|
49
72
|
|
|
50
73
|
return withRouteAssurance<Middleware>(
|
|
51
74
|
{
|
package/src/timing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ContextPlugin, defineContextPlugin } from "@nifrajs/core/server"
|
|
2
2
|
import { withHeaders } from "./_utils.ts"
|
|
3
3
|
|
|
4
4
|
export interface TimingMetric {
|
|
@@ -62,7 +62,7 @@ function formatMetric(metric: TimingMetric, precision: number): string {
|
|
|
62
62
|
* Put request-rewriting middleware (for example `methodOverride`) before `timing()` so timing is
|
|
63
63
|
* attached to the final routed request.
|
|
64
64
|
*/
|
|
65
|
-
export function timing(options: TimingOptions = {}) {
|
|
65
|
+
export function timing(options: TimingOptions = {}): ContextPlugin<{ timing: TimingControls }> {
|
|
66
66
|
const totalName = options.total === undefined ? "total" : options.total
|
|
67
67
|
if (totalName !== false) assertMetricName(totalName)
|
|
68
68
|
const precision = options.precision ?? 1
|
|
@@ -104,7 +104,7 @@ export function timing(options: TimingOptions = {}) {
|
|
|
104
104
|
},
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
-
return
|
|
107
|
+
return defineContextPlugin<{ timing: TimingControls }>("timing", (app) =>
|
|
108
108
|
app
|
|
109
109
|
.onRequest((req) => {
|
|
110
110
|
if (!isEnabled(req)) return undefined
|
package/src/token-auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance"
|
|
2
|
-
import {
|
|
2
|
+
import { defineIdentityPlugin, type IdentityPlugin } from "@nifrajs/core/server"
|
|
3
3
|
|
|
4
4
|
type MaybePromise<T> = T | Promise<T>
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ type MaybePromise<T> = T | Promise<T>
|
|
|
19
19
|
* const app = server().use(auth).get("/me", (c) => auth.requirePrincipal(c.req))
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
export type AuthPlugin<P> =
|
|
22
|
+
export type AuthPlugin<P> = IdentityPlugin & {
|
|
23
23
|
/** The verified principal for this request, or `null` (no/invalid token in `optional` mode). */
|
|
24
24
|
principal(request: Request): P | null
|
|
25
25
|
/** The verified principal, or **throws a `401` `Response`** when absent. */
|
|
@@ -48,7 +48,7 @@ function createTokenAuth<P>(config: TokenAuthConfig<P>): AuthPlugin<P> {
|
|
|
48
48
|
headers: config.challenge !== undefined ? { "www-authenticate": config.challenge } : {},
|
|
49
49
|
},
|
|
50
50
|
)
|
|
51
|
-
const plugin =
|
|
51
|
+
const plugin = defineIdentityPlugin(config.name, (app) =>
|
|
52
52
|
app.beforeHandle(async (c: { readonly req: Request }) => {
|
|
53
53
|
const token = config.extract(c.req)
|
|
54
54
|
// Empty string is treated as "no credential" - never passed to verify.
|
package/src/trailing-slash.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineRouterPlugin } from "@nifrajs/core/server"
|
|
2
2
|
|
|
3
3
|
export interface TrailingSlashOptions {
|
|
4
4
|
/** Redirect (default) or route internally with a rewritten URL. */
|
|
@@ -59,7 +59,7 @@ export function trimTrailingSlash(options: TrailingSlashOptions = {}) {
|
|
|
59
59
|
validateStatus(status)
|
|
60
60
|
const methods = methodsOf(options.methods)
|
|
61
61
|
|
|
62
|
-
return
|
|
62
|
+
return defineRouterPlugin("trimTrailingSlash", (app) =>
|
|
63
63
|
app.onRequest((req) => {
|
|
64
64
|
if (!methods.has(req.method.toUpperCase())) return undefined
|
|
65
65
|
const path = new URL(req.url).pathname
|
|
@@ -81,7 +81,7 @@ export function appendTrailingSlash(options: TrailingSlashOptions = {}) {
|
|
|
81
81
|
validateStatus(status)
|
|
82
82
|
const methods = methodsOf(options.methods)
|
|
83
83
|
|
|
84
|
-
return
|
|
84
|
+
return defineRouterPlugin("appendTrailingSlash", (app) =>
|
|
85
85
|
app.onRequest((req) => {
|
|
86
86
|
if (!methods.has(req.method.toUpperCase())) return undefined
|
|
87
87
|
const path = new URL(req.url).pathname
|