@nifrajs/core 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.
Files changed (180) hide show
  1. package/dist/assurance.d.ts +61 -3
  2. package/dist/assurance.d.ts.map +1 -1
  3. package/dist/assurance.js +152 -5
  4. package/dist/assurance.js.map +1 -1
  5. package/dist/capabilities.d.ts +9 -1
  6. package/dist/capabilities.d.ts.map +1 -1
  7. package/dist/capabilities.js +5 -1
  8. package/dist/capabilities.js.map +1 -1
  9. package/dist/diff.d.ts +2 -1
  10. package/dist/diff.d.ts.map +1 -1
  11. package/dist/diff.js +3 -1
  12. package/dist/diff.js.map +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/evidence.d.ts +89 -0
  17. package/dist/evidence.d.ts.map +1 -0
  18. package/dist/evidence.js +170 -0
  19. package/dist/evidence.js.map +1 -0
  20. package/dist/execution-policy.d.ts +23 -0
  21. package/dist/execution-policy.d.ts.map +1 -0
  22. package/dist/execution-policy.js +28 -0
  23. package/dist/execution-policy.js.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/internal/route-assurance.d.ts +19 -0
  27. package/dist/internal/route-assurance.d.ts.map +1 -1
  28. package/dist/internal/route-assurance.js +38 -5
  29. package/dist/internal/route-assurance.js.map +1 -1
  30. package/dist/internal/route-execution.d.ts +18 -0
  31. package/dist/internal/route-execution.d.ts.map +1 -1
  32. package/dist/internal/route-execution.js +67 -1
  33. package/dist/internal/route-execution.js.map +1 -1
  34. package/dist/manifest.d.ts +5 -1
  35. package/dist/manifest.d.ts.map +1 -1
  36. package/dist/manifest.js +22 -4
  37. package/dist/manifest.js.map +1 -1
  38. package/dist/reflection.d.ts +5 -0
  39. package/dist/reflection.d.ts.map +1 -1
  40. package/dist/reflection.js +13 -4
  41. package/dist/reflection.js.map +1 -1
  42. package/dist/replay.d.ts +26 -0
  43. package/dist/replay.d.ts.map +1 -0
  44. package/dist/replay.js +63 -0
  45. package/dist/replay.js.map +1 -0
  46. package/dist/schema/standard.d.ts +2 -0
  47. package/dist/schema/standard.d.ts.map +1 -1
  48. package/dist/schema/standard.js +13 -0
  49. package/dist/schema/standard.js.map +1 -1
  50. package/dist/server/body.d.ts +49 -6
  51. package/dist/server/body.d.ts.map +1 -1
  52. package/dist/server/body.js +226 -8
  53. package/dist/server/body.js.map +1 -1
  54. package/dist/server/context.d.ts +20 -3
  55. package/dist/server/context.d.ts.map +1 -1
  56. package/dist/server/contract.d.ts +15 -3
  57. package/dist/server/contract.d.ts.map +1 -1
  58. package/dist/server/contract.js +22 -0
  59. package/dist/server/contract.js.map +1 -1
  60. package/dist/server/cookies.d.ts +26 -5
  61. package/dist/server/cookies.d.ts.map +1 -1
  62. package/dist/server/cookies.js +76 -9
  63. package/dist/server/cookies.js.map +1 -1
  64. package/dist/server/edge.d.ts.map +1 -1
  65. package/dist/server/edge.js +11 -0
  66. package/dist/server/edge.js.map +1 -1
  67. package/dist/server/header-case.d.ts +9 -0
  68. package/dist/server/header-case.d.ts.map +1 -0
  69. package/dist/server/header-case.js +54 -0
  70. package/dist/server/header-case.js.map +1 -0
  71. package/dist/server/headers.d.ts +8 -0
  72. package/dist/server/headers.d.ts.map +1 -0
  73. package/dist/server/headers.js +13 -0
  74. package/dist/server/headers.js.map +1 -0
  75. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  76. package/dist/server/node-outcome-hook.js +7 -11
  77. package/dist/server/node-outcome-hook.js.map +1 -1
  78. package/dist/server/node-outcome.d.ts +8 -1
  79. package/dist/server/node-outcome.d.ts.map +1 -1
  80. package/dist/server/node-outcome.js +10 -3
  81. package/dist/server/node-outcome.js.map +1 -1
  82. package/dist/server/plugin.d.ts +66 -7
  83. package/dist/server/plugin.d.ts.map +1 -1
  84. package/dist/server/plugin.js +14 -10
  85. package/dist/server/plugin.js.map +1 -1
  86. package/dist/server/proto-guard.d.ts +30 -0
  87. package/dist/server/proto-guard.d.ts.map +1 -0
  88. package/dist/server/proto-guard.js +116 -0
  89. package/dist/server/proto-guard.js.map +1 -0
  90. package/dist/server/registry.d.ts +6 -2
  91. package/dist/server/registry.d.ts.map +1 -1
  92. package/dist/server/request-context.d.ts +24 -6
  93. package/dist/server/request-context.d.ts.map +1 -1
  94. package/dist/server/request-context.js +157 -30
  95. package/dist/server/request-context.js.map +1 -1
  96. package/dist/server/respond.d.ts +2 -2
  97. package/dist/server/respond.d.ts.map +1 -1
  98. package/dist/server/respond.js +34 -8
  99. package/dist/server/respond.js.map +1 -1
  100. package/dist/server/response-contract-lane.d.ts +1 -1
  101. package/dist/server/response-contract-lane.d.ts.map +1 -1
  102. package/dist/server/response-contract-lane.js +45 -13
  103. package/dist/server/response-contract-lane.js.map +1 -1
  104. package/dist/server/response-hooks.d.ts +10 -0
  105. package/dist/server/response-hooks.d.ts.map +1 -0
  106. package/dist/server/response-hooks.js +52 -0
  107. package/dist/server/response-hooks.js.map +1 -0
  108. package/dist/server/runtime-core.d.ts +12 -0
  109. package/dist/server/runtime-core.d.ts.map +1 -1
  110. package/dist/server/runtime-core.js +8 -0
  111. package/dist/server/runtime-core.js.map +1 -1
  112. package/dist/server/server-types.d.ts +60 -6
  113. package/dist/server/server-types.d.ts.map +1 -1
  114. package/dist/server/server.d.ts +80 -23
  115. package/dist/server/server.d.ts.map +1 -1
  116. package/dist/server/server.js +424 -213
  117. package/dist/server/server.js.map +1 -1
  118. package/dist/server/static-headers.d.ts +14 -3
  119. package/dist/server/static-headers.d.ts.map +1 -1
  120. package/dist/server/static-headers.js +28 -6
  121. package/dist/server/static-headers.js.map +1 -1
  122. package/dist/server/transport-lane.d.ts +8 -0
  123. package/dist/server/transport-lane.d.ts.map +1 -1
  124. package/dist/server/transport-lane.js +12 -8
  125. package/dist/server/transport-lane.js.map +1 -1
  126. package/dist/server/websocket.d.ts +3 -0
  127. package/dist/server/websocket.d.ts.map +1 -1
  128. package/dist/server/websocket.js +14 -0
  129. package/dist/server/websocket.js.map +1 -1
  130. package/dist/server.d.ts +2 -2
  131. package/dist/server.d.ts.map +1 -1
  132. package/dist/server.js +2 -2
  133. package/dist/server.js.map +1 -1
  134. package/dist/tool-catalog.d.ts +21 -0
  135. package/dist/tool-catalog.d.ts.map +1 -0
  136. package/dist/tool-catalog.js +16 -0
  137. package/dist/tool-catalog.js.map +1 -0
  138. package/dist/tool-contract.d.ts +238 -0
  139. package/dist/tool-contract.d.ts.map +1 -0
  140. package/dist/tool-contract.js +569 -0
  141. package/dist/tool-contract.js.map +1 -0
  142. package/package.json +26 -1
  143. package/src/assurance.ts +236 -6
  144. package/src/capabilities.ts +14 -1
  145. package/src/diff.ts +5 -2
  146. package/src/errors.ts +1 -0
  147. package/src/evidence.ts +319 -0
  148. package/src/execution-policy.ts +57 -0
  149. package/src/index.ts +1 -1
  150. package/src/internal/route-assurance.ts +67 -4
  151. package/src/internal/route-execution.ts +276 -1
  152. package/src/manifest.ts +27 -5
  153. package/src/reflection.ts +24 -4
  154. package/src/replay.ts +96 -0
  155. package/src/schema/standard.ts +16 -0
  156. package/src/server/body.ts +275 -7
  157. package/src/server/context.ts +23 -3
  158. package/src/server/contract.ts +43 -3
  159. package/src/server/cookies.ts +80 -10
  160. package/src/server/edge.ts +10 -0
  161. package/src/server/header-case.ts +54 -0
  162. package/src/server/headers.ts +11 -0
  163. package/src/server/node-outcome-hook.ts +6 -11
  164. package/src/server/node-outcome.ts +10 -2
  165. package/src/server/plugin.ts +87 -8
  166. package/src/server/proto-guard.ts +118 -0
  167. package/src/server/registry.ts +6 -2
  168. package/src/server/request-context.ts +242 -26
  169. package/src/server/respond.ts +35 -4
  170. package/src/server/response-contract-lane.ts +6 -17
  171. package/src/server/response-hooks.ts +51 -0
  172. package/src/server/runtime-core.ts +14 -0
  173. package/src/server/server-types.ts +66 -6
  174. package/src/server/server.ts +667 -287
  175. package/src/server/static-headers.ts +27 -3
  176. package/src/server/transport-lane.ts +19 -8
  177. package/src/server/websocket.ts +18 -2
  178. package/src/server.ts +7 -0
  179. package/src/tool-catalog.ts +40 -0
  180. package/src/tool-contract.ts +896 -0
@@ -9,6 +9,8 @@
9
9
  * hook) is applied later and therefore wins.
10
10
  */
11
11
 
12
+ import { markLowercaseHeaderKeys } from "./header-case.ts"
13
+
12
14
  /** Valid HTTP field-name characters (RFC 9110 token). */
13
15
  const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
14
16
 
@@ -118,25 +120,47 @@ function store<V>(record: Record<string, V>, name: string, value: V): void {
118
120
  * than left alongside - one header name spelled two ways in a single record ships as a comma-joined
119
121
  * value on the Web paths and as two lines on Node, so writing the static entry blind would change
120
122
  * the wire instead of being overridden.
123
+ *
124
+ * `markLowercase` asks for the all-lowercase proof to be published on the result when it holds. The
125
+ * merge is the natural place to answer that: the static names were lowercased at registration, and
126
+ * the loop below already derives `lower` for every own name to decide the override, so `lower !==
127
+ * name` is the whole answer at no added cost. Only the Node-direct lane asks - its readers are the
128
+ * ones that would otherwise re-walk these keys - and only for an app with no raw native response
129
+ * twin, since such a twin writes the record past the case-normalizing view AFTER this runs.
121
130
  */
122
131
  export function mergeStaticHeaderRecord<V>(
123
132
  statics: Readonly<Record<string, string>>,
124
133
  own: Readonly<Record<string, V>>,
134
+ markLowercase = false,
125
135
  ): Record<string, V | string> {
126
136
  const merged: Record<string, V | string> = { ...statics }
137
+ let allLowercase = true
127
138
  for (const name of Object.keys(own)) {
128
139
  const value = own[name] as V
129
140
  if (value === undefined) continue
130
141
  const lower = name.toLowerCase()
131
- if (lower !== name && Object.hasOwn(merged, lower)) delete merged[lower]
142
+ if (lower !== name) {
143
+ allLowercase = false
144
+ if (Object.hasOwn(merged, lower)) delete merged[lower]
145
+ }
132
146
  store(merged, name, value)
133
147
  }
148
+ if (markLowercase && allLowercase) {
149
+ markLowercaseHeaderKeys(merged as Record<string, string | readonly string[]>)
150
+ }
134
151
  return merged
135
152
  }
136
153
 
137
- /** A fresh, mutable copy of the static record - what the lanes whose writers mutate the record get. */
154
+ /**
155
+ * A fresh, mutable copy of the static record - what the lanes whose writers mutate the record get.
156
+ * Every name in it was lowercased at registration, so `markLowercase` publishes a proof that needs no
157
+ * scan at all; the gate on it is the same one {@link mergeStaticHeaderRecord} documents.
158
+ */
138
159
  export function staticHeaderRecordCopy(
139
160
  statics: StaticResponseHeaders,
161
+ markLowercase = false,
140
162
  ): Record<string, string | readonly string[]> {
141
- return { ...statics.record }
163
+ const copy = { ...statics.record }
164
+ if (markLowercase) markLowercaseHeaderKeys(copy)
165
+ return copy
142
166
  }
@@ -2,7 +2,8 @@
2
2
  import type { TransportCodec, TransportCodecRegistry } from "../transport-codec.ts"
3
3
  import { readBoundedBytes } from "./body.ts"
4
4
  import { jsonError } from "./http.ts"
5
- import { isResponseResult } from "./runtime-core.ts"
5
+ import { guardParsedValue, type ProtoPoisoning } from "./proto-guard.ts"
6
+ import { isResponseResult, PRE_DECODED_BODY, type PreDecodedBody } from "./runtime-core.ts"
6
7
  import type { AnyServer, IdentityPlugin } from "./server.ts"
7
8
 
8
9
  interface TransportBodySource {
@@ -28,6 +29,13 @@ export interface TransportRuntime {
28
29
  export interface TransportCodecsOptions {
29
30
  /** Maximum encoded request bytes. Keep aligned with `server({ maxBodyBytes })`. */
30
31
  readonly maxBytes?: number
32
+ /**
33
+ * Prototype-poisoning policy for decoded request bodies, mirroring
34
+ * `server({ protoPoisoning })` - this lane parses with the codec's own decoder, so the body
35
+ * lane's guard never sees the raw text and the policy must be enforced here. Default `"reject"`:
36
+ * a poisoned payload answers the same flat 400 as an undecodable one.
37
+ */
38
+ readonly protoPoisoning?: ProtoPoisoning
31
39
  }
32
40
 
33
41
  export function transportCodecs(
@@ -38,6 +46,7 @@ export function transportCodecs(
38
46
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
39
47
  throw new RangeError("transport maxBytes must be a non-negative safe integer")
40
48
  }
49
+ const protoPoisoning = options.protoPoisoning ?? "reject"
41
50
  const runtime: TransportRuntime = Object.freeze({
42
51
  responseCodec(accept: string | null): TransportCodec {
43
52
  try {
@@ -70,9 +79,12 @@ export function transportCodecs(
70
79
  }
71
80
  }
72
81
  try {
82
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(read.bytes)
83
+ // Same poisoning policy as the body lane, enforced on the codec's own decode - a rejected
84
+ // payload answers exactly like an undecodable one.
73
85
  return {
74
86
  matched: true,
75
- value: codec.decode(new TextDecoder("utf-8", { fatal: true }).decode(read.bytes)),
87
+ value: guardParsedValue(codec.decode(text), protoPoisoning),
76
88
  }
77
89
  } catch {
78
90
  return {
@@ -103,9 +115,9 @@ export function transportCodecs(
103
115
  if (!decoded.matched) return undefined
104
116
  if ("response" in decoded) return decoded.response
105
117
 
106
- // Keep an untouched body branch while the bounded decoder consumes the other tee branch.
107
- // Existing JSON validation lanes can then use their native `request.json()` fast path while
108
- // receiving the already-decoded rich value without importing codec machinery into the kernel.
118
+ // Re-frame as a plain JSON request whose already-decoded value rides the pre-decoded stash;
119
+ // the body lane takes the stash verbatim (this lane owns the cap and poisoning policy), so
120
+ // the placeholder body is never parsed and codec machinery stays out of the kernel.
109
121
  const headers: Record<string, string> = {}
110
122
  replacement.headers.forEach((value, name) => {
111
123
  headers[name] = value
@@ -119,9 +131,8 @@ export function transportCodecs(
119
131
  body: "{}",
120
132
  signal: replacement.signal as never,
121
133
  })
122
- Object.defineProperty(normalized, "json", {
123
- value: () => Promise.resolve(decoded.value),
124
- })
134
+ const stash: PreDecodedBody = { value: decoded.value }
135
+ Object.defineProperty(normalized, PRE_DECODED_BODY, { value: stash })
125
136
  return normalized
126
137
  })
127
138
  app.afterHandle((result, context) => {
@@ -207,6 +207,7 @@ export type WebSocketUpgradeOutcome =
207
207
  * a standard socket without a static import of the WS implementation (which would defeat the
208
208
  * `.use(websocket())` tree-shaking). `@nifrajs/workers` may import `attachWebSocket` directly instead. */
209
209
  readonly attach: WsAttach
210
+ readonly maxPayloadBytes?: number
210
211
  }
211
212
 
212
213
  /** The socket-wiring signature the upgrade outcome carries (the installed runtime's `attach`). */
@@ -214,7 +215,7 @@ export type WsAttach = (
214
215
  socket: StandardWebSocket,
215
216
  handler: WebSocketHandler,
216
217
  data: unknown,
217
- options: { openNow: boolean; pubsub: TopicRegistry },
218
+ options: { openNow: boolean; pubsub: TopicRegistry; maxPayloadBytes?: number },
218
219
  ) => NifraWebSocket
219
220
 
220
221
  /**
@@ -249,7 +250,7 @@ export function attachWebSocket(
249
250
  socket: StandardWebSocket,
250
251
  handler: WebSocketHandler,
251
252
  data: unknown,
252
- options: { openNow: boolean; pubsub: TopicRegistry },
253
+ options: { openNow: boolean; pubsub: TopicRegistry; maxPayloadBytes?: number },
253
254
  ): NifraWebSocket {
254
255
  const { pubsub } = options
255
256
  const ws: NifraWebSocket = {
@@ -282,9 +283,23 @@ export function attachWebSocket(
282
283
  }
283
284
  // Deliver binary as ArrayBuffer (→ Uint8Array) rather than Blob, where the runtime allows it.
284
285
  if ("binaryType" in socket) socket.binaryType = "arraybuffer"
286
+ const maxPayloadBytes = options.maxPayloadBytes
285
287
  socket.addEventListener("message", (event: { readonly data: unknown }) => {
286
288
  const raw = event.data
287
289
  const payload: WebSocketData = typeof raw === "string" ? raw : toBinary(raw)
290
+ // Measuring a text frame costs a full UTF-8 encode plus the buffer it allocates, so it happens
291
+ // only for a route that actually declared a cap. Reading `.byteLength` off an already-decoded
292
+ // binary frame is free, but the branch stays under the same guard to keep one shape here.
293
+ if (maxPayloadBytes !== undefined) {
294
+ const payloadBytes =
295
+ typeof raw === "string"
296
+ ? WS_PAYLOAD_ENCODER.encode(raw).byteLength
297
+ : (payload as Uint8Array).byteLength
298
+ if (payloadBytes > maxPayloadBytes) {
299
+ socket.close(1009, "message too large")
300
+ return
301
+ }
302
+ }
288
303
  safe(() => handler.message?.(ws, payload))
289
304
  })
290
305
  socket.addEventListener(
@@ -304,6 +319,7 @@ export function attachWebSocket(
304
319
  }
305
320
 
306
321
  const WS_MESSAGE_DECODER = new TextDecoder()
322
+ const WS_PAYLOAD_ENCODER = new TextEncoder()
307
323
 
308
324
  /**
309
325
  * If the handler declares a `messageSchema`, return a copy whose `message` validates each frame -
package/src/server.ts CHANGED
@@ -31,6 +31,8 @@ export type {
31
31
  } from "./server/context.ts"
32
32
  export {
33
33
  type CookieOptions,
34
+ type CookieSecret,
35
+ cookieNamePrefix,
34
36
  parseCookies,
35
37
  serializeCookie,
36
38
  signValue,
@@ -73,6 +75,9 @@ export {
73
75
  type AdmissionController,
74
76
  type AdmissionDecision,
75
77
  type AnyServer,
78
+ type ContextPlugin,
79
+ type DefinePluginResult,
80
+ defineContextPlugin,
76
81
  defineIdentityPlugin,
77
82
  definePlugin,
78
83
  defineRouterPlugin,
@@ -81,6 +86,7 @@ export {
81
86
  type McpPromptDescriptor,
82
87
  type McpResourceDescriptor,
83
88
  type Middleware,
89
+ type NifraFeatureVersion,
84
90
  type NifraPlugin,
85
91
  type NodeRequestContext,
86
92
  type NodeRequestHook,
@@ -88,6 +94,7 @@ export {
88
94
  type NodeResponseHook,
89
95
  type NodeServeOutcome,
90
96
  type OnRequestResult,
97
+ type PluginTypeCollapsed,
91
98
  type PromptArgument,
92
99
  type PromptMessage,
93
100
  type ResponseBodyHook,
@@ -0,0 +1,40 @@
1
+ /** Descriptive projections of typed tool contracts for model and transport catalogs. */
2
+
3
+ import {
4
+ type ToolAnnotations,
5
+ type ToolApprovalPolicy,
6
+ type ToolContract,
7
+ type ToolSensitivity,
8
+ toolInputJsonSchema,
9
+ } from "./tool-contract.ts"
10
+
11
+ /**
12
+ * A read-only, transport-neutral view of a tool contract.
13
+ *
14
+ * This module is descriptive only. It never validates input, checks capabilities, requests approval,
15
+ * reserves idempotency, consumes cost, or executes the tool. Those policies stay in `executeTool`.
16
+ */
17
+ export interface ToolCatalogEntry {
18
+ readonly name: string
19
+ readonly description: string
20
+ readonly inputSchema: Record<string, unknown>
21
+ readonly capability: string
22
+ readonly sensitivity: ToolSensitivity
23
+ readonly approval: ToolApprovalPolicy
24
+ readonly idempotent: boolean
25
+ readonly annotations: ToolAnnotations
26
+ }
27
+
28
+ /** Build the single descriptive projection shared by agent, MCP, and test adapters. */
29
+ export function toCatalogEntry<Input, Output>(tool: ToolContract<Input, Output>): ToolCatalogEntry {
30
+ return Object.freeze({
31
+ name: tool.name,
32
+ description: tool.description,
33
+ inputSchema: Object.freeze({ ...toolInputJsonSchema(tool) }),
34
+ capability: tool.capability,
35
+ sensitivity: tool.sensitivity,
36
+ approval: tool.approval,
37
+ idempotent: tool.idempotency !== undefined,
38
+ annotations: tool.annotations,
39
+ })
40
+ }