@nifrajs/core 3.3.0 → 4.0.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 (331) hide show
  1. package/README.md +26 -0
  2. package/dist/assurance.d.ts +7 -0
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +39 -3
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +20 -2
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +47 -15
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/diff.d.ts +6 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +36 -2
  13. package/dist/diff.js.map +1 -1
  14. package/dist/durable-adapters.d.ts.map +1 -1
  15. package/dist/durable-adapters.js +16 -3
  16. package/dist/durable-adapters.js.map +1 -1
  17. package/dist/durable-effect.d.ts.map +1 -1
  18. package/dist/durable-effect.js +6 -3
  19. package/dist/durable-effect.js.map +1 -1
  20. package/dist/edge-kit.d.ts +2 -0
  21. package/dist/edge-kit.d.ts.map +1 -1
  22. package/dist/edge-kit.js +2 -0
  23. package/dist/edge-kit.js.map +1 -1
  24. package/dist/effect-lifecycle.d.ts.map +1 -1
  25. package/dist/effect-lifecycle.js +2 -1
  26. package/dist/effect-lifecycle.js.map +1 -1
  27. package/dist/effect-scope.d.ts.map +1 -1
  28. package/dist/effect-scope.js +1 -1
  29. package/dist/effect-scope.js.map +1 -1
  30. package/dist/errors.d.ts +1 -1
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js.map +1 -1
  33. package/dist/evidence.d.ts +36 -1
  34. package/dist/evidence.d.ts.map +1 -1
  35. package/dist/evidence.js +219 -13
  36. package/dist/evidence.js.map +1 -1
  37. package/dist/idempotency.d.ts +31 -0
  38. package/dist/idempotency.d.ts.map +1 -1
  39. package/dist/idempotency.js +48 -9
  40. package/dist/idempotency.js.map +1 -1
  41. package/dist/index.d.ts +9 -6
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/code-unit-order.d.ts +7 -0
  46. package/dist/internal/code-unit-order.d.ts.map +1 -0
  47. package/dist/internal/code-unit-order.js +9 -0
  48. package/dist/internal/code-unit-order.js.map +1 -0
  49. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  50. package/dist/internal/durable-saga-protocol.js +2 -0
  51. package/dist/internal/durable-saga-protocol.js.map +1 -1
  52. package/dist/internal/route-catalog.d.ts +6 -6
  53. package/dist/internal/route-catalog.d.ts.map +1 -1
  54. package/dist/internal/route-catalog.js +20 -18
  55. package/dist/internal/route-catalog.js.map +1 -1
  56. package/dist/internal/route-compiler.d.ts +7 -6
  57. package/dist/internal/route-compiler.d.ts.map +1 -1
  58. package/dist/internal/route-compiler.js +36 -4
  59. package/dist/internal/route-compiler.js.map +1 -1
  60. package/dist/internal/route-execution.d.ts +7 -3
  61. package/dist/internal/route-execution.d.ts.map +1 -1
  62. package/dist/internal/route-execution.js.map +1 -1
  63. package/dist/internal/route-lanes.d.ts +1 -0
  64. package/dist/internal/route-lanes.d.ts.map +1 -1
  65. package/dist/internal/route-lanes.js +34 -29
  66. package/dist/internal/route-lanes.js.map +1 -1
  67. package/dist/internal/route-program.d.ts +8 -2
  68. package/dist/internal/route-program.d.ts.map +1 -1
  69. package/dist/internal/route-program.js +222 -20
  70. package/dist/internal/route-program.js.map +1 -1
  71. package/dist/ledger.d.ts.map +1 -1
  72. package/dist/ledger.js +8 -5
  73. package/dist/ledger.js.map +1 -1
  74. package/dist/manifest.d.ts +1 -1
  75. package/dist/manifest.d.ts.map +1 -1
  76. package/dist/manifest.js +52 -46
  77. package/dist/manifest.js.map +1 -1
  78. package/dist/mount.d.ts +36 -0
  79. package/dist/mount.d.ts.map +1 -1
  80. package/dist/mount.js +32 -0
  81. package/dist/mount.js.map +1 -1
  82. package/dist/reflection.d.ts +26 -0
  83. package/dist/reflection.d.ts.map +1 -1
  84. package/dist/reflection.js +65 -1
  85. package/dist/reflection.js.map +1 -1
  86. package/dist/response-observer.d.ts +1 -1
  87. package/dist/response-observer.d.ts.map +1 -1
  88. package/dist/response-observer.js.map +1 -1
  89. package/dist/router/overlap.d.ts +23 -0
  90. package/dist/router/overlap.d.ts.map +1 -0
  91. package/dist/router/overlap.js +224 -0
  92. package/dist/router/overlap.js.map +1 -0
  93. package/dist/router/pattern.d.ts +117 -2
  94. package/dist/router/pattern.d.ts.map +1 -1
  95. package/dist/router/pattern.js +283 -23
  96. package/dist/router/pattern.js.map +1 -1
  97. package/dist/router/router.d.ts +18 -4
  98. package/dist/router/router.d.ts.map +1 -1
  99. package/dist/router/router.js +51 -44
  100. package/dist/router/router.js.map +1 -1
  101. package/dist/server/auth.d.ts +53 -0
  102. package/dist/server/auth.d.ts.map +1 -0
  103. package/dist/server/auth.js +9 -0
  104. package/dist/server/auth.js.map +1 -0
  105. package/dist/server/bare-error-lane.d.ts +12 -7
  106. package/dist/server/bare-error-lane.d.ts.map +1 -1
  107. package/dist/server/bare-error-lane.js +13 -11
  108. package/dist/server/bare-error-lane.js.map +1 -1
  109. package/dist/server/body-lane.d.ts +20 -2
  110. package/dist/server/body-lane.d.ts.map +1 -1
  111. package/dist/server/body-lane.js +26 -7
  112. package/dist/server/body-lane.js.map +1 -1
  113. package/dist/server/body-parser.d.ts +68 -0
  114. package/dist/server/body-parser.d.ts.map +1 -0
  115. package/dist/server/body-parser.js +172 -0
  116. package/dist/server/body-parser.js.map +1 -0
  117. package/dist/server/body.d.ts.map +1 -1
  118. package/dist/server/body.js +62 -7
  119. package/dist/server/body.js.map +1 -1
  120. package/dist/server/client-ip.d.ts +8 -0
  121. package/dist/server/client-ip.d.ts.map +1 -1
  122. package/dist/server/client-ip.js +8 -0
  123. package/dist/server/client-ip.js.map +1 -1
  124. package/dist/server/context.d.ts +71 -6
  125. package/dist/server/context.d.ts.map +1 -1
  126. package/dist/server/contract.d.ts +15 -5
  127. package/dist/server/contract.d.ts.map +1 -1
  128. package/dist/server/contract.js +25 -4
  129. package/dist/server/contract.js.map +1 -1
  130. package/dist/server/cookies.d.ts +4 -1
  131. package/dist/server/cookies.d.ts.map +1 -1
  132. package/dist/server/cookies.js +40 -2
  133. package/dist/server/cookies.js.map +1 -1
  134. package/dist/server/core-copies.d.ts +2 -0
  135. package/dist/server/core-copies.d.ts.map +1 -0
  136. package/dist/server/core-copies.js +20 -0
  137. package/dist/server/core-copies.js.map +1 -0
  138. package/dist/server/edge.d.ts.map +1 -1
  139. package/dist/server/edge.js +2 -0
  140. package/dist/server/edge.js.map +1 -1
  141. package/dist/server/http.d.ts +11 -0
  142. package/dist/server/http.d.ts.map +1 -1
  143. package/dist/server/http.js +36 -0
  144. package/dist/server/http.js.map +1 -1
  145. package/dist/server/idempotency-lane.d.ts +6 -0
  146. package/dist/server/idempotency-lane.d.ts.map +1 -1
  147. package/dist/server/idempotency-lane.js +130 -73
  148. package/dist/server/idempotency-lane.js.map +1 -1
  149. package/dist/server/install.d.ts +4 -0
  150. package/dist/server/install.d.ts.map +1 -1
  151. package/dist/server/install.js +4 -0
  152. package/dist/server/install.js.map +1 -1
  153. package/dist/server/methods.d.ts +45 -0
  154. package/dist/server/methods.d.ts.map +1 -0
  155. package/dist/server/methods.js +37 -0
  156. package/dist/server/methods.js.map +1 -0
  157. package/dist/server/multipart.d.ts +63 -0
  158. package/dist/server/multipart.d.ts.map +1 -0
  159. package/dist/server/multipart.js +374 -0
  160. package/dist/server/multipart.js.map +1 -0
  161. package/dist/server/node-outcome-hook.d.ts +20 -0
  162. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  163. package/dist/server/node-outcome-hook.js +15 -6
  164. package/dist/server/node-outcome-hook.js.map +1 -1
  165. package/dist/server/not-found-answer.d.ts +40 -0
  166. package/dist/server/not-found-answer.d.ts.map +1 -0
  167. package/dist/server/not-found-answer.js +56 -0
  168. package/dist/server/not-found-answer.js.map +1 -0
  169. package/dist/server/not-found.d.ts +23 -0
  170. package/dist/server/not-found.d.ts.map +1 -0
  171. package/dist/server/not-found.js +81 -0
  172. package/dist/server/not-found.js.map +1 -0
  173. package/dist/server/plugin.d.ts +26 -1
  174. package/dist/server/plugin.d.ts.map +1 -1
  175. package/dist/server/plugin.js.map +1 -1
  176. package/dist/server/proto-guard.d.ts +18 -5
  177. package/dist/server/proto-guard.d.ts.map +1 -1
  178. package/dist/server/proto-guard.js +114 -9
  179. package/dist/server/proto-guard.js.map +1 -1
  180. package/dist/server/query.d.ts +7 -0
  181. package/dist/server/query.d.ts.map +1 -1
  182. package/dist/server/query.js +47 -0
  183. package/dist/server/query.js.map +1 -1
  184. package/dist/server/registry.d.ts +13 -2
  185. package/dist/server/registry.d.ts.map +1 -1
  186. package/dist/server/request-context.d.ts +11 -0
  187. package/dist/server/request-context.d.ts.map +1 -1
  188. package/dist/server/request-context.js +33 -19
  189. package/dist/server/request-context.js.map +1 -1
  190. package/dist/server/request-lineage.d.ts +8 -0
  191. package/dist/server/request-lineage.d.ts.map +1 -0
  192. package/dist/server/request-lineage.js +14 -0
  193. package/dist/server/request-lineage.js.map +1 -0
  194. package/dist/server/request-target.d.ts +31 -0
  195. package/dist/server/request-target.d.ts.map +1 -0
  196. package/dist/server/request-target.js +44 -0
  197. package/dist/server/request-target.js.map +1 -0
  198. package/dist/server/respond.d.ts +1 -1
  199. package/dist/server/respond.d.ts.map +1 -1
  200. package/dist/server/respond.js +16 -4
  201. package/dist/server/respond.js.map +1 -1
  202. package/dist/server/response-contract-lane.d.ts +19 -1
  203. package/dist/server/response-contract-lane.d.ts.map +1 -1
  204. package/dist/server/response-contract-lane.js +59 -14
  205. package/dist/server/response-contract-lane.js.map +1 -1
  206. package/dist/server/response-observer-runtime.d.ts +9 -5
  207. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  208. package/dist/server/response-observer-runtime.js +10 -6
  209. package/dist/server/response-observer-runtime.js.map +1 -1
  210. package/dist/server/runtime-core.d.ts +2 -0
  211. package/dist/server/runtime-core.d.ts.map +1 -1
  212. package/dist/server/runtime-core.js.map +1 -1
  213. package/dist/server/server-types.d.ts +55 -3
  214. package/dist/server/server-types.d.ts.map +1 -1
  215. package/dist/server/server.d.ts +194 -19
  216. package/dist/server/server.d.ts.map +1 -1
  217. package/dist/server/server.js +1342 -274
  218. package/dist/server/server.js.map +1 -1
  219. package/dist/server/sse.d.ts.map +1 -1
  220. package/dist/server/sse.js +10 -7
  221. package/dist/server/sse.js.map +1 -1
  222. package/dist/server/static-headers.d.ts +3 -0
  223. package/dist/server/static-headers.d.ts.map +1 -1
  224. package/dist/server/static-headers.js +19 -1
  225. package/dist/server/static-headers.js.map +1 -1
  226. package/dist/server/transport-lane.d.ts +8 -0
  227. package/dist/server/transport-lane.d.ts.map +1 -1
  228. package/dist/server/transport-lane.js +9 -8
  229. package/dist/server/transport-lane.js.map +1 -1
  230. package/dist/server/validation.d.ts.map +1 -1
  231. package/dist/server/validation.js +5 -1
  232. package/dist/server/validation.js.map +1 -1
  233. package/dist/server/websocket.d.ts +10 -3
  234. package/dist/server/websocket.d.ts.map +1 -1
  235. package/dist/server/websocket.js +7 -4
  236. package/dist/server/websocket.js.map +1 -1
  237. package/dist/server/ws-bun.d.ts +2 -0
  238. package/dist/server/ws-bun.d.ts.map +1 -1
  239. package/dist/server/ws-bun.js +6 -3
  240. package/dist/server/ws-bun.js.map +1 -1
  241. package/dist/server/ws-hook.d.ts +2 -1
  242. package/dist/server/ws-hook.d.ts.map +1 -1
  243. package/dist/server.d.ts +8 -5
  244. package/dist/server.d.ts.map +1 -1
  245. package/dist/server.js +5 -2
  246. package/dist/server.js.map +1 -1
  247. package/dist/single-copy-register.js +5 -0
  248. package/dist/single-copy-register.js.map +1 -1
  249. package/dist/single-copy.d.ts +46 -3
  250. package/dist/single-copy.d.ts.map +1 -1
  251. package/dist/single-copy.js +125 -12
  252. package/dist/single-copy.js.map +1 -1
  253. package/dist/tool-contract.d.ts +23 -0
  254. package/dist/tool-contract.d.ts.map +1 -1
  255. package/dist/tool-contract.js +37 -3
  256. package/dist/tool-contract.js.map +1 -1
  257. package/dist/transport-codec.d.ts.map +1 -1
  258. package/dist/transport-codec.js +2 -1
  259. package/dist/transport-codec.js.map +1 -1
  260. package/package.json +27 -2
  261. package/src/assurance.ts +52 -2
  262. package/src/capabilities.ts +81 -22
  263. package/src/diff.ts +54 -7
  264. package/src/durable-adapters.ts +20 -3
  265. package/src/durable-effect.ts +10 -3
  266. package/src/edge-kit.ts +7 -0
  267. package/src/effect-lifecycle.ts +5 -1
  268. package/src/effect-scope.ts +2 -1
  269. package/src/errors.ts +3 -0
  270. package/src/evidence.ts +302 -12
  271. package/src/idempotency.ts +80 -9
  272. package/src/index.ts +41 -3
  273. package/src/internal/code-unit-order.ts +8 -0
  274. package/src/internal/durable-saga-protocol.ts +2 -0
  275. package/src/internal/route-catalog.ts +19 -18
  276. package/src/internal/route-compiler.ts +62 -13
  277. package/src/internal/route-execution.ts +13 -3
  278. package/src/internal/route-lanes.ts +37 -28
  279. package/src/internal/route-program.ts +434 -20
  280. package/src/ledger.ts +11 -5
  281. package/src/manifest.ts +52 -46
  282. package/src/mount.ts +62 -0
  283. package/src/reflection.ts +84 -1
  284. package/src/response-observer.ts +1 -0
  285. package/src/router/overlap.ts +258 -0
  286. package/src/router/pattern.ts +349 -25
  287. package/src/router/router.ts +76 -51
  288. package/src/server/auth.ts +75 -0
  289. package/src/server/bare-error-lane.ts +12 -11
  290. package/src/server/body-lane.ts +46 -6
  291. package/src/server/body-parser.ts +241 -0
  292. package/src/server/body.ts +82 -9
  293. package/src/server/client-ip.ts +9 -0
  294. package/src/server/context.ts +219 -12
  295. package/src/server/contract.ts +54 -9
  296. package/src/server/cookies.ts +34 -2
  297. package/src/server/core-copies.ts +21 -0
  298. package/src/server/edge.ts +2 -0
  299. package/src/server/http.ts +34 -0
  300. package/src/server/idempotency-lane.ts +147 -72
  301. package/src/server/install.ts +6 -0
  302. package/src/server/methods.ts +143 -0
  303. package/src/server/multipart.ts +424 -0
  304. package/src/server/node-outcome-hook.ts +40 -7
  305. package/src/server/not-found-answer.ts +109 -0
  306. package/src/server/not-found.ts +121 -0
  307. package/src/server/plugin.ts +45 -1
  308. package/src/server/proto-guard.ts +128 -9
  309. package/src/server/query.ts +49 -0
  310. package/src/server/registry.ts +17 -2
  311. package/src/server/request-context.ts +44 -16
  312. package/src/server/request-lineage.ts +15 -0
  313. package/src/server/request-target.ts +46 -0
  314. package/src/server/respond.ts +14 -3
  315. package/src/server/response-contract-lane.ts +97 -16
  316. package/src/server/response-observer-runtime.ts +37 -8
  317. package/src/server/runtime-core.ts +2 -0
  318. package/src/server/server-types.ts +61 -2
  319. package/src/server/server.ts +1798 -355
  320. package/src/server/sse.ts +10 -8
  321. package/src/server/static-headers.ts +22 -1
  322. package/src/server/transport-lane.ts +17 -9
  323. package/src/server/validation.ts +7 -1
  324. package/src/server/websocket.ts +23 -9
  325. package/src/server/ws-bun.ts +8 -3
  326. package/src/server/ws-hook.ts +2 -1
  327. package/src/server.ts +39 -2
  328. package/src/single-copy-register.ts +5 -0
  329. package/src/single-copy.ts +160 -12
  330. package/src/tool-contract.ts +66 -3
  331. package/src/transport-codec.ts +2 -1
@@ -48,7 +48,7 @@ import {
48
48
  } from "../schema/standard.ts"
49
49
  import { INSTALL_RESPONSE_CONTRACT } from "./install.ts"
50
50
  import type { IdentityPlugin } from "./plugin.ts"
51
- import { isResponseResult } from "./runtime-core.ts"
51
+ import { isResponseResult, status } from "./runtime-core.ts"
52
52
  import type { AnyServer } from "./server.ts"
53
53
 
54
54
  /**
@@ -62,6 +62,12 @@ import type { AnyServer } from "./server.ts"
62
62
  */
63
63
  export type ResponseContractMode = "warn" | "enforce"
64
64
 
65
+ /** Success and status-specific error schemas captured by one route at registration time. */
66
+ export interface ResponseContractDefinition {
67
+ readonly response?: StandardSchemaV1
68
+ readonly errors?: Readonly<Record<number, StandardSchemaV1>>
69
+ }
70
+
65
71
  /** The outcome of checking one handler result against its declared response schema. */
66
72
  export type ResponseContractOutcome =
67
73
  /** Nothing to report. `value` is what should be serialized (the original result, or the cleaned one). */
@@ -91,38 +97,104 @@ export function checkResponseContract(
91
97
  result: unknown,
92
98
  mode: "warn" | "enforce",
93
99
  ): ResponseContractOutcome | Promise<ResponseContractOutcome> {
100
+ return checkRouteResponseContract({ response: schema }, result, mode)
101
+ }
102
+
103
+ /** The schema a reply with this status is held to: `response` for 2xx, else `errors[status]`. */
104
+ const schemaFor = (definition: ResponseContractDefinition, code: number) =>
105
+ code >= 200 && code < 300 ? definition.response : definition.errors?.[code]
106
+
107
+ /** A `c.json(...)` reply rebuilt around its validated value, keeping its status and headers. */
108
+ function reserialize(reply: Response, value: unknown): Response {
109
+ const headers = new Headers(reply.headers)
110
+ headers.delete("content-length")
111
+ return new Response(JSON.stringify(value), {
112
+ status: reply.status,
113
+ statusText: reply.statusText,
114
+ headers,
115
+ })
116
+ }
117
+
118
+ /**
119
+ * Check a route's success or status-specific error payload. Plain `status()` results retain their
120
+ * status while enforcement replaces only the body with the validator's output. A `c.json(...)`
121
+ * reply is held to the contract through the value it was built from (`jsonReply`). Any other raw
122
+ * `Response` remains an explicit transport escape hatch because inspecting it would consume
123
+ * streams or alter redirects; assurance/static checks must govern that path separately.
124
+ */
125
+ export function checkRouteResponseContract(
126
+ definition: ResponseContractDefinition,
127
+ result: unknown,
128
+ mode: "warn" | "enforce",
129
+ jsonReply?: { readonly body: unknown },
130
+ ): ResponseContractOutcome | Promise<ResponseContractOutcome> {
131
+ const plain = isResponseResult(result) ? result.plain : undefined
132
+ if (plain !== undefined) {
133
+ const schema = schemaFor(definition, plain.status)
134
+ if (schema === undefined) return { kind: "ok", value: result }
135
+ return validateContract(schema, plain.body, result, mode, (value) =>
136
+ status(
137
+ plain.status,
138
+ value,
139
+ plain.headers === undefined ? undefined : { headers: plain.headers },
140
+ ),
141
+ )
142
+ }
143
+ if (jsonReply !== undefined && result instanceof Response) {
144
+ const schema = schemaFor(definition, result.status)
145
+ if (schema === undefined) return { kind: "ok", value: result }
146
+ return validateContract(schema, jsonReply.body, result, mode, (value) =>
147
+ reserialize(result, value),
148
+ )
149
+ }
150
+
94
151
  // A handler may return a raw Response as deliberate control flow (a redirect, a stream). There is no
95
152
  // JSON payload to hold to the contract, and re-serializing one would corrupt it. A `status(...)` is
96
- // the same control flow in plain-data form: the contract describes the route's SUCCESS payload, so
97
- // holding an early exit (a 401 body, say) to it would fail every guarded route that declares one.
98
- if (result instanceof Response || result === undefined || isResponseResult(result))
99
- return { kind: "ok", value: result }
100
- const settled = schema["~standard"].validate(result)
153
+ // intentionally handled above because its plain body and status remain available without consuming
154
+ // a stream. An absent body has nothing to validate.
155
+ if (result instanceof Response || result === undefined) return { kind: "ok", value: result }
156
+ const schema = definition.response
157
+ if (schema === undefined) return { kind: "ok", value: result }
158
+ return validateContract(schema, result, result, mode, (value) => value)
159
+ }
160
+
161
+ /** Validate `payload`; `served` is what goes out unchanged when nothing is enforced - the whole
162
+ * `status(...)` result or reply, never just its body. */
163
+ function validateContract(
164
+ schema: StandardSchemaV1,
165
+ payload: unknown,
166
+ served: unknown,
167
+ mode: "warn" | "enforce",
168
+ replace: (value: unknown) => unknown,
169
+ ): ResponseContractOutcome | Promise<ResponseContractOutcome> {
170
+ const settled = schema["~standard"].validate(payload)
101
171
  return settled instanceof Promise
102
- ? settled.then((r) => interpret(r, result, mode))
103
- : interpret(settled, result, mode)
172
+ ? settled.then((r) => interpret(r, payload, served, mode, replace))
173
+ : interpret(settled, payload, served, mode, replace)
104
174
  }
105
175
 
106
176
  function interpret(
107
177
  settled: StandardResult<unknown>,
108
- result: unknown,
178
+ payload: unknown,
179
+ served: unknown,
109
180
  mode: "warn" | "enforce",
181
+ replace: (value: unknown) => unknown,
110
182
  ): ResponseContractOutcome {
111
183
  if (settled.issues !== undefined) {
112
184
  const message = `response does not satisfy its declared contract: ${formatStandardIssues(settled.issues)}`
113
185
  // `warn` never changes what is served - it reports and gets out of the way, so switching it on can
114
186
  // never be the thing that broke production.
115
187
  return mode === "warn"
116
- ? { kind: "warn", value: result, message }
188
+ ? { kind: "warn", value: served, message }
117
189
  : { kind: "violation", message }
118
190
  }
119
- const dropped = droppedKeys(result, settled.value)
120
- if (mode === "enforce") return { kind: "ok", value: settled.value }
191
+ const dropped = droppedKeys(payload, settled.value)
192
+ if (mode === "enforce") return { kind: "ok", value: replace(settled.value) }
121
193
  return dropped.length === 0
122
- ? { kind: "ok", value: result }
194
+ ? { kind: "ok", value: served }
123
195
  : {
124
196
  kind: "warn",
125
- value: result,
197
+ value: served,
126
198
  message: `response carries fields its contract does not declare: ${dropped.join(", ")}. They are being sent; \`responseContract: "enforce"\` would strip them.`,
127
199
  }
128
200
  }
@@ -134,8 +206,9 @@ function interpret(
134
206
  export interface ResponseContractRuntime {
135
207
  readonly mode: ResponseContractMode
136
208
  check(
137
- schema: StandardSchemaV1,
209
+ definition: ResponseContractDefinition,
138
210
  result: unknown,
211
+ context?: { readonly jsonReply?: Response | undefined; readonly jsonBody?: unknown },
139
212
  ): ResponseContractOutcome | Promise<ResponseContractOutcome>
140
213
  }
141
214
 
@@ -161,7 +234,15 @@ interface ResponseContractInstallable {
161
234
  export function responseContract(mode: ResponseContractMode = "warn"): IdentityPlugin {
162
235
  const runtime: ResponseContractRuntime = {
163
236
  mode,
164
- check: (schema, result) => checkResponseContract(schema, result, mode),
237
+ check: (definition, result, context) =>
238
+ checkRouteResponseContract(
239
+ definition,
240
+ result,
241
+ mode,
242
+ result instanceof Response && result === context?.jsonReply
243
+ ? { body: context.jsonBody }
244
+ : undefined,
245
+ ),
165
246
  }
166
247
  const apply = <S extends AnyServer>(app: S): S => {
167
248
  ;(app as unknown as ResponseContractInstallable)[INSTALL_RESPONSE_CONTRACT](runtime)
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  NodeRequestContext,
3
+ NodeResponseBodyHook,
3
4
  NodeResponseHook,
4
5
  ResponseBodyHook,
5
6
  ResponseHeadersHook,
@@ -12,18 +13,30 @@ import { applyBodyReplacement, withReplacedBody } from "./response-hooks.ts"
12
13
  type MaybePromise<T> = T | Promise<T>
13
14
  type WebResponseHook = (response: Response, req: Request) => MaybePromise<Response>
14
15
 
15
- export type { ResponseBodyHook, ResponseHeadersHook } from "./node-outcome-hook.ts"
16
+ export type {
17
+ NodeResponseBodyHook,
18
+ ResponseBodyHook,
19
+ ResponseHeadersHook,
20
+ } from "./node-outcome-hook.ts"
16
21
 
17
22
  export interface ResponseObserverHost {
18
23
  assertConfigurable(operation: string): void
19
- addResponseHook(web: WebResponseHook, node: NodeResponseHook | undefined): void
24
+ addResponseHook(
25
+ web: WebResponseHook,
26
+ node: NodeResponseHook | undefined,
27
+ headerOnly?: boolean,
28
+ ): void
20
29
  enableResponseBodyTagging(): object
21
30
  responseBodyOwners(): ReadonlySet<object>
22
31
  }
23
32
 
24
33
  export interface ResponseObserverMethods {
25
- onResponseHeaders(fn: ResponseHeadersHook): this
26
- onResponseBody(fn: ResponseBodyHook): this
34
+ /**
35
+ * Register a portable header hook. `node` is an optional equivalent for the Node-direct record;
36
+ * it must preserve the portable hook's observable header semantics.
37
+ */
38
+ onResponseHeaders(fn: ResponseHeadersHook, node?: NodeResponseHook): this
39
+ onResponseBody(fn: ResponseBodyHook, node?: NodeResponseBodyHook): this
27
40
  onResponseRaw(fn: (response: Response, req: Request) => MaybePromise<Response>): this
28
41
  }
29
42
 
@@ -81,7 +94,11 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
81
94
  return {
82
95
  install(host) {
83
96
  return {
84
- onResponseHeaders(this: ResponseObserverMethods, fn): ResponseObserverMethods {
97
+ onResponseHeaders(
98
+ this: ResponseObserverMethods,
99
+ fn: ResponseHeadersHook,
100
+ node?: NodeResponseHook,
101
+ ): ResponseObserverMethods {
85
102
  host.assertConfigurable("onResponseHeaders()")
86
103
  host.addResponseHook(
87
104
  (response, req) => {
@@ -89,11 +106,16 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
89
106
  const out = fn(view.headers, view.request, response.status)
90
107
  return out instanceof Promise ? out.then(() => view.response) : view.response
91
108
  },
92
- (response, req) => fn(recordHeadersView(response), req, response.status),
109
+ node ?? ((response, req) => fn(recordHeadersView(response), req, response.status)),
110
+ true,
93
111
  )
94
112
  return this
95
113
  },
96
- onResponseBody(this: ResponseObserverMethods, fn): ResponseObserverMethods {
114
+ onResponseBody(
115
+ this: ResponseObserverMethods,
116
+ fn: ResponseBodyHook,
117
+ node?: NodeResponseBodyHook,
118
+ ): ResponseObserverMethods {
97
119
  host.assertConfigurable("onResponseBody()")
98
120
  host.enableResponseBodyTagging()
99
121
  host.addResponseHook(
@@ -108,12 +130,18 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
108
130
  },
109
131
  (response, req) => {
110
132
  if (response.body === null) return undefined
111
- const out = fn(response.body, recordHeadersView(response), req, response.status)
133
+ // An author-supplied twin sees the same bytes and returns through the same
134
+ // replacement channel; only the header-view allocation is skipped.
135
+ const out =
136
+ node === undefined
137
+ ? fn(response.body, recordHeadersView(response), req, response.status)
138
+ : node(response.body, response, req, response.status)
112
139
  if (out instanceof Promise)
113
140
  return out.then((replaced) => applyBodyReplacement(response, replaced))
114
141
  applyBodyReplacement(response, out)
115
142
  return undefined
116
143
  },
144
+ false,
117
145
  )
118
146
  return this
119
147
  },
@@ -126,6 +154,7 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
126
154
  ? fn(response, req)
127
155
  : response,
128
156
  () => undefined,
157
+ false,
129
158
  )
130
159
  return this
131
160
  },
@@ -27,6 +27,8 @@ export const PRE_DECODED_BODY = Symbol("nifra.body.preDecoded")
27
27
  /** The stash shape under {@link PRE_DECODED_BODY}. */
28
28
  export interface PreDecodedBody {
29
29
  readonly value: unknown
30
+ /** The encoded size, so the route's own body limit still applies to a body decoded before routing. */
31
+ readonly byteLength: number
30
32
  }
31
33
 
32
34
  /**
@@ -5,17 +5,19 @@
5
5
  */
6
6
  import type { CapabilityUseEvent } from "../internal/capability-runtime.ts"
7
7
  import type { AssuranceEvidence } from "../internal/route-assurance.ts"
8
- import type { Method } from "../router/router.ts"
8
+ import type { RouteMethod } from "../router/router.ts"
9
9
  import type { ClientIpTrust } from "./client-ip.ts"
10
10
  import type { Context, Platform, RouteSchema } from "./context.ts"
11
11
  import type { Logger } from "./logger.ts"
12
12
  import type {
13
13
  NodeRequestHook,
14
+ NodeResponseBodyHook,
14
15
  NodeResponseHook,
15
16
  ResponseBodyHook,
16
17
  ResponseHeadersHook,
17
18
  } from "./node-outcome-hook.ts"
18
19
  import type { MaybePromise, OnRequestResult } from "./server.ts"
20
+ import type { WebSocketUpgradeOutcome } from "./websocket.ts"
19
21
 
20
22
  /**
21
23
  * The outcome of a capacity-admission decision. `admitted` requests carry a `release` the server calls
@@ -85,6 +87,8 @@ export interface ServerOptions {
85
87
  readonly wsMaxPayloadBytes?: number
86
88
  /** Per-request timeout (ms): a slower request gets a 503 and `ctx.signal` aborts. 0 disables (default). */
87
89
  readonly requestTimeoutMs?: number
90
+ /** WebSocket upgrade admission/guard timeout (ms). Default 10_000; set to 0 only for a deliberate unbounded guard. */
91
+ readonly wsUpgradeTimeoutMs?: number
88
92
  /**
89
93
  * How to derive `c.clientIp` behind a reverse proxy or CDN. Omit (default) to trust only the raw
90
94
  * socket peer and never believe a forwarded header. Set `{ trustedHops: n }` when the app sits
@@ -167,10 +171,46 @@ export type FetchHandler<Env = unknown> = (
167
171
  platform?: Platform<Env>,
168
172
  ) => MaybePromise<Response>
169
173
 
174
+ /** A fetch app that may also participate in adapter-owned WebSocket upgrades when mounted. */
175
+ export interface MountableApp<Env = unknown> {
176
+ readonly fetch: FetchHandler<Env>
177
+ readonly resolveWebSocketUpgrade?: (
178
+ request: Request,
179
+ platform?: Platform<Env>,
180
+ ) => MaybePromise<WebSocketUpgradeOutcome>
181
+ }
182
+
183
+ /** Options for a first-class composed mount. */
184
+ export interface MountOptions<Env = unknown> {
185
+ /** Absolute pathname prefix, optionally ending in `/*`. */
186
+ readonly path: string
187
+ /** The child app. Its HTTP fetch and optional WebSocket resolver share the mount. */
188
+ readonly app: MountableApp<Env>
189
+ /** Rewrite the child request to remove `path` before dispatch. Default: false. */
190
+ readonly stripPrefix?: boolean
191
+ /** Higher values run first; ties use the most specific path, then declaration order. */
192
+ readonly priority?: number
193
+ /** For safe replayable methods only, try the next matching mount when this one returns 404. */
194
+ readonly fallbackOn?: 404
195
+ /**
196
+ * Why this child is not analyzed, for a handler whose effects nifra cannot follow (a closure from
197
+ * another package). Capability assurance lists a mount with a reason as a known gap; a mount
198
+ * without one fails it, since its routes are invisible. A nifra `server()` belongs in `merge()`,
199
+ * where its routes are analyzed.
200
+ */
201
+ readonly opaque?: string
202
+ }
203
+
170
204
  /** Options for a legacy fetch-handler mount. */
171
205
  export interface MountFetchOptions {
172
206
  /** Remove the mount prefix before invoking the legacy handler. Default: false. */
173
207
  readonly stripPrefix?: boolean
208
+ /** Higher values run first; ties use the most specific path, then declaration order. */
209
+ readonly priority?: number
210
+ /** For safe replayable methods only, try the next matching legacy mount when it returns 404. */
211
+ readonly fallbackOn?: 404
212
+ /** Why this handler is not analyzed - see {@link MountOptions.opaque}. */
213
+ readonly opaque?: string
174
214
  }
175
215
 
176
216
  /** A callback awaited after the Bun server has drained and stopped. */
@@ -200,9 +240,11 @@ export interface ToolAnnotations {
200
240
  * (e.g. `toOpenAPI`) enumerate routes after registration.
201
241
  */
202
242
  export interface RouteDescriptor {
203
- readonly method: Method
243
+ readonly method: RouteMethod
204
244
  readonly path: string
205
245
  readonly schema: RouteSchema | undefined
246
+ /** Runtime response-contract mode captured when this route was registered. */
247
+ readonly responseContract?: "warn" | "enforce"
206
248
  /** Effective enforcement evidence, populated only during reflection/introspection. */
207
249
  readonly assurance?: readonly AssuranceEvidence[]
208
250
  /** Normalized declared effect tokens, populated only when the route declares any. */
@@ -246,6 +288,19 @@ export interface McpPromptDescriptor {
246
288
  readonly handler: (args: Record<string, string>) => MaybePromise<readonly PromptMessage[]>
247
289
  }
248
290
 
291
+ /**
292
+ * TLS for `listen()`: serve HTTPS from Bun itself, with no proxy in front. `cert` and `key` are PEM,
293
+ * as text or as the file's bytes (`readFileSync("cert.pem")`); `passphrase` unlocks an encrypted key.
294
+ */
295
+ export interface ListenTlsOptions {
296
+ /** The certificate chain, PEM. */
297
+ readonly cert: string | Uint8Array
298
+ /** The private key, PEM. */
299
+ readonly key: string | Uint8Array
300
+ /** The passphrase of an encrypted `key`. */
301
+ readonly passphrase?: string
302
+ }
303
+
249
304
  /**
250
305
  * The handle `listen()` returns - the slice of Bun's server nifra holds and exposes.
251
306
  * Declared explicitly (rather than `ReturnType<typeof Bun.serve>`) so the public type
@@ -301,6 +356,8 @@ export interface Middleware {
301
356
  * an optional `onNodeResponse` twin only when the hook must see a real `Response`.
302
357
  */
303
358
  readonly onResponseHeaders?: ResponseHeadersHook
359
+ /** Allocation-light Node-direct twin for a portable header-only hook; keep its wire semantics identical. */
360
+ readonly onNodeResponseHeaders?: NodeResponseHook
304
361
  /**
305
362
  * PORTABLE post-serialization body hook - the payload tier. Receives the final
306
363
  * framework-serialized bytes (already resident on every runtime; no stream is drained) plus the
@@ -308,6 +365,8 @@ export interface Middleware {
308
365
  * (proxies, SSE, streamed SSR) are skipped by contract - transforming those needs `onResponse`.
309
366
  */
310
367
  readonly onResponseBody?: ResponseBodyHook
368
+ /** Allocation-light Node-direct twin for a portable body hook; keep its wire semantics identical. */
369
+ readonly onNodeResponseBody?: NodeResponseBodyHook
311
370
  /**
312
371
  * Response transform for untagged/raw responses (streams, proxied fetches, and framework errors).
313
372
  * Framework-serialized payloads use `onResponseBody` and remain on the Node direct lane.