@nifrajs/middleware 2.11.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.
Files changed (120) hide show
  1. package/dist/_utils.d.ts +14 -2
  2. package/dist/_utils.d.ts.map +1 -1
  3. package/dist/_utils.js +25 -10
  4. package/dist/_utils.js.map +1 -1
  5. package/dist/basic-auth.d.ts +2 -2
  6. package/dist/basic-auth.d.ts.map +1 -1
  7. package/dist/basic-auth.js +2 -2
  8. package/dist/basic-auth.js.map +1 -1
  9. package/dist/cache-control.d.ts +1 -1
  10. package/dist/cache-control.d.ts.map +1 -1
  11. package/dist/cache-control.js +2 -2
  12. package/dist/cache-control.js.map +1 -1
  13. package/dist/cache.d.ts +2 -0
  14. package/dist/cache.d.ts.map +1 -1
  15. package/dist/cache.js +13 -1
  16. package/dist/cache.js.map +1 -1
  17. package/dist/combine.d.ts +11 -5
  18. package/dist/combine.d.ts.map +1 -1
  19. package/dist/combine.js +11 -4
  20. package/dist/combine.js.map +1 -1
  21. package/dist/compression.d.ts +8 -1
  22. package/dist/compression.d.ts.map +1 -1
  23. package/dist/compression.js +9 -2
  24. package/dist/compression.js.map +1 -1
  25. package/dist/csrf.d.ts +11 -4
  26. package/dist/csrf.d.ts.map +1 -1
  27. package/dist/csrf.js +19 -5
  28. package/dist/csrf.js.map +1 -1
  29. package/dist/durable-command.d.ts +2 -2
  30. package/dist/durable-command.d.ts.map +1 -1
  31. package/dist/durable-command.js +2 -2
  32. package/dist/durable-command.js.map +1 -1
  33. package/dist/etag.d.ts +2 -2
  34. package/dist/etag.d.ts.map +1 -1
  35. package/dist/etag.js +3 -3
  36. package/dist/etag.js.map +1 -1
  37. package/dist/healthcheck.d.ts +2 -1
  38. package/dist/healthcheck.d.ts.map +1 -1
  39. package/dist/healthcheck.js +5 -2
  40. package/dist/healthcheck.js.map +1 -1
  41. package/dist/idempotency.d.ts +51 -5
  42. package/dist/idempotency.d.ts.map +1 -1
  43. package/dist/idempotency.js +110 -13
  44. package/dist/idempotency.js.map +1 -1
  45. package/dist/jwt.d.ts +2 -2
  46. package/dist/jwt.d.ts.map +1 -1
  47. package/dist/jwt.js +14 -3
  48. package/dist/jwt.js.map +1 -1
  49. package/dist/language.d.ts +9 -1
  50. package/dist/language.d.ts.map +1 -1
  51. package/dist/language.js +8 -2
  52. package/dist/language.js.map +1 -1
  53. package/dist/logger.d.ts +9 -4
  54. package/dist/logger.d.ts.map +1 -1
  55. package/dist/logger.js +9 -5
  56. package/dist/logger.js.map +1 -1
  57. package/dist/method-override.d.ts +1 -1
  58. package/dist/method-override.d.ts.map +1 -1
  59. package/dist/method-override.js +2 -2
  60. package/dist/method-override.js.map +1 -1
  61. package/dist/openapi.d.ts +2 -1
  62. package/dist/openapi.d.ts.map +1 -1
  63. package/dist/openapi.js +4 -2
  64. package/dist/openapi.js.map +1 -1
  65. package/dist/pretty-json.d.ts +1 -1
  66. package/dist/pretty-json.d.ts.map +1 -1
  67. package/dist/pretty-json.js +2 -2
  68. package/dist/pretty-json.js.map +1 -1
  69. package/dist/problem-details.d.ts +1 -1
  70. package/dist/problem-details.d.ts.map +1 -1
  71. package/dist/problem-details.js +2 -2
  72. package/dist/problem-details.js.map +1 -1
  73. package/dist/rate-limit.d.ts.map +1 -1
  74. package/dist/rate-limit.js +6 -8
  75. package/dist/rate-limit.js.map +1 -1
  76. package/dist/request-id.d.ts +12 -4
  77. package/dist/request-id.d.ts.map +1 -1
  78. package/dist/request-id.js +62 -7
  79. package/dist/request-id.js.map +1 -1
  80. package/dist/security-headers.d.ts +11 -0
  81. package/dist/security-headers.d.ts.map +1 -1
  82. package/dist/security-headers.js +12 -0
  83. package/dist/security-headers.js.map +1 -1
  84. package/dist/timing.d.ts +4 -1
  85. package/dist/timing.d.ts.map +1 -1
  86. package/dist/timing.js +2 -2
  87. package/dist/timing.js.map +1 -1
  88. package/dist/token-auth.d.ts +2 -2
  89. package/dist/token-auth.d.ts.map +1 -1
  90. package/dist/token-auth.js +2 -2
  91. package/dist/token-auth.js.map +1 -1
  92. package/dist/trailing-slash.d.ts +2 -2
  93. package/dist/trailing-slash.d.ts.map +1 -1
  94. package/dist/trailing-slash.js +3 -3
  95. package/dist/trailing-slash.js.map +1 -1
  96. package/package.json +3 -3
  97. package/src/_utils.ts +27 -10
  98. package/src/basic-auth.ts +3 -3
  99. package/src/cache-control.ts +2 -2
  100. package/src/cache.ts +20 -1
  101. package/src/combine.ts +22 -10
  102. package/src/compression.ts +9 -2
  103. package/src/csrf.ts +31 -15
  104. package/src/durable-command.ts +3 -3
  105. package/src/etag.ts +3 -3
  106. package/src/healthcheck.ts +6 -3
  107. package/src/idempotency.ts +146 -18
  108. package/src/jwt.ts +22 -4
  109. package/src/language.ts +26 -16
  110. package/src/logger.ts +10 -6
  111. package/src/method-override.ts +2 -2
  112. package/src/openapi.ts +5 -3
  113. package/src/pretty-json.ts +2 -2
  114. package/src/problem-details.ts +2 -2
  115. package/src/rate-limit.ts +6 -8
  116. package/src/request-id.ts +63 -11
  117. package/src/security-headers.ts +23 -0
  118. package/src/timing.ts +3 -3
  119. package/src/token-auth.ts +3 -3
  120. package/src/trailing-slash.ts +3 -3
package/src/request-id.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { definePlugin } from "@nifrajs/core/server"
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 definePlugin} plugin that gives every request a stable id: it reuses an inbound
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 the response header. Idempotent - applying it twice is a
14
- * no-op.
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
- return definePlugin("requestId", (app) =>
24
- app.derive((c) => {
25
- const id = c.header(header) ?? generate()
26
- c.set.headers[header] = id // echo on the response (route handlers)
27
- return { requestId: id }
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
  }
@@ -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 { definePlugin } from "@nifrajs/core/server"
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 definePlugin("timing", (app) =>
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 { definePlugin, type NifraPlugin } from "@nifrajs/core/server"
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> = NifraPlugin & {
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 = definePlugin(config.name, (app) =>
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.
@@ -1,4 +1,4 @@
1
- import { definePlugin } from "@nifrajs/core/server"
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 definePlugin("trimTrailingSlash", (app) =>
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 definePlugin("appendTrailingSlash", (app) =>
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