@nifrajs/core 2.13.0 → 3.1.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 (177) hide show
  1. package/README.md +4 -0
  2. package/dist/assurance.d.ts +16 -8
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +33 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/binary.d.ts +32 -0
  7. package/dist/binary.d.ts.map +1 -1
  8. package/dist/binary.js +17 -0
  9. package/dist/binary.js.map +1 -1
  10. package/dist/durable-adapters.d.ts.map +1 -1
  11. package/dist/durable-adapters.js +21 -50
  12. package/dist/durable-adapters.js.map +1 -1
  13. package/dist/durable-execution.d.ts.map +1 -1
  14. package/dist/durable-execution.js +49 -69
  15. package/dist/durable-execution.js.map +1 -1
  16. package/dist/edge-kit.d.ts +27 -0
  17. package/dist/edge-kit.d.ts.map +1 -0
  18. package/dist/edge-kit.js +24 -0
  19. package/dist/edge-kit.js.map +1 -0
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/durable-approval-protocol.d.ts +13 -0
  28. package/dist/internal/durable-approval-protocol.d.ts.map +1 -0
  29. package/dist/internal/durable-approval-protocol.js +66 -0
  30. package/dist/internal/durable-approval-protocol.js.map +1 -0
  31. package/dist/internal/durable-effect-protocol.d.ts +14 -0
  32. package/dist/internal/durable-effect-protocol.d.ts.map +1 -0
  33. package/dist/internal/durable-effect-protocol.js +16 -0
  34. package/dist/internal/durable-effect-protocol.js.map +1 -0
  35. package/dist/internal/durable-saga-protocol.d.ts +8 -0
  36. package/dist/internal/durable-saga-protocol.d.ts.map +1 -0
  37. package/dist/internal/durable-saga-protocol.js +21 -0
  38. package/dist/internal/durable-saga-protocol.js.map +1 -0
  39. package/dist/internal/route-catalog.d.ts +3 -0
  40. package/dist/internal/route-catalog.d.ts.map +1 -1
  41. package/dist/internal/route-catalog.js +5 -0
  42. package/dist/internal/route-catalog.js.map +1 -1
  43. package/dist/internal/route-execution.d.ts +11 -5
  44. package/dist/internal/route-execution.d.ts.map +1 -1
  45. package/dist/internal/route-execution.js +2 -1
  46. package/dist/internal/route-execution.js.map +1 -1
  47. package/dist/response-observer.d.ts +25 -0
  48. package/dist/response-observer.d.ts.map +1 -0
  49. package/dist/response-observer.js +40 -0
  50. package/dist/response-observer.js.map +1 -0
  51. package/dist/security/baseline.d.ts +68 -0
  52. package/dist/security/baseline.d.ts.map +1 -0
  53. package/dist/security/baseline.js +118 -0
  54. package/dist/security/baseline.js.map +1 -0
  55. package/dist/server/bare-error-lane.d.ts +26 -0
  56. package/dist/server/bare-error-lane.d.ts.map +1 -0
  57. package/dist/server/bare-error-lane.js +60 -0
  58. package/dist/server/bare-error-lane.js.map +1 -0
  59. package/dist/server/body.d.ts +7 -0
  60. package/dist/server/body.d.ts.map +1 -1
  61. package/dist/server/body.js +7 -0
  62. package/dist/server/body.js.map +1 -1
  63. package/dist/server/bridge-markers.d.ts +25 -0
  64. package/dist/server/bridge-markers.d.ts.map +1 -0
  65. package/dist/server/bridge-markers.js +25 -0
  66. package/dist/server/bridge-markers.js.map +1 -0
  67. package/dist/server/context.d.ts +6 -1
  68. package/dist/server/context.d.ts.map +1 -1
  69. package/dist/server/header-case.d.ts.map +1 -1
  70. package/dist/server/header-case.js +2 -1
  71. package/dist/server/header-case.js.map +1 -1
  72. package/dist/server/http.d.ts +36 -3
  73. package/dist/server/http.d.ts.map +1 -1
  74. package/dist/server/http.js +49 -3
  75. package/dist/server/http.js.map +1 -1
  76. package/dist/server/install.d.ts +6 -8
  77. package/dist/server/install.d.ts.map +1 -1
  78. package/dist/server/install.js +7 -0
  79. package/dist/server/install.js.map +1 -1
  80. package/dist/server/node-outcome-hook.d.ts +2 -1
  81. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  82. package/dist/server/node-outcome-hook.js.map +1 -1
  83. package/dist/server/node-outcome.d.ts +17 -2
  84. package/dist/server/node-outcome.d.ts.map +1 -1
  85. package/dist/server/node-outcome.js +37 -3
  86. package/dist/server/node-outcome.js.map +1 -1
  87. package/dist/server/plugin.d.ts +1 -1
  88. package/dist/server/plugin.d.ts.map +1 -1
  89. package/dist/server/query.d.ts +7 -1
  90. package/dist/server/query.d.ts.map +1 -1
  91. package/dist/server/query.js +8 -3
  92. package/dist/server/query.js.map +1 -1
  93. package/dist/server/registry.d.ts +11 -2
  94. package/dist/server/registry.d.ts.map +1 -1
  95. package/dist/server/request-context.d.ts +17 -4
  96. package/dist/server/request-context.d.ts.map +1 -1
  97. package/dist/server/request-context.js +82 -45
  98. package/dist/server/request-context.js.map +1 -1
  99. package/dist/server/respond.d.ts.map +1 -1
  100. package/dist/server/respond.js +19 -2
  101. package/dist/server/respond.js.map +1 -1
  102. package/dist/server/response-contract-lane.d.ts.map +1 -1
  103. package/dist/server/response-contract-lane.js +5 -2
  104. package/dist/server/response-contract-lane.js.map +1 -1
  105. package/dist/server/response-observer-runtime.d.ts +20 -0
  106. package/dist/server/response-observer-runtime.d.ts.map +1 -0
  107. package/dist/server/response-observer-runtime.js +95 -0
  108. package/dist/server/response-observer-runtime.js.map +1 -0
  109. package/dist/server/runtime-core.d.ts +60 -0
  110. package/dist/server/runtime-core.d.ts.map +1 -1
  111. package/dist/server/runtime-core.js +65 -1
  112. package/dist/server/runtime-core.js.map +1 -1
  113. package/dist/server/server-types.d.ts +18 -1
  114. package/dist/server/server-types.d.ts.map +1 -1
  115. package/dist/server/server.d.ts +89 -48
  116. package/dist/server/server.d.ts.map +1 -1
  117. package/dist/server/server.js +397 -276
  118. package/dist/server/server.js.map +1 -1
  119. package/dist/server/websocket.d.ts +20 -4
  120. package/dist/server/websocket.d.ts.map +1 -1
  121. package/dist/server/websocket.js +81 -11
  122. package/dist/server/websocket.js.map +1 -1
  123. package/dist/server/ws-bun.d.ts +1 -1
  124. package/dist/server/ws-bun.d.ts.map +1 -1
  125. package/dist/server/ws-bun.js +12 -8
  126. package/dist/server/ws-bun.js.map +1 -1
  127. package/dist/server.d.ts +2 -1
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +2 -1
  130. package/dist/server.js.map +1 -1
  131. package/dist/single-copy-register.d.ts +2 -0
  132. package/dist/single-copy-register.d.ts.map +1 -0
  133. package/dist/single-copy-register.js +24 -0
  134. package/dist/single-copy-register.js.map +1 -0
  135. package/dist/single-copy.d.ts +156 -0
  136. package/dist/single-copy.d.ts.map +1 -0
  137. package/dist/single-copy.js +470 -0
  138. package/dist/single-copy.js.map +1 -0
  139. package/package.json +24 -2
  140. package/src/assurance.ts +58 -1
  141. package/src/binary.ts +35 -0
  142. package/src/durable-adapters.ts +26 -51
  143. package/src/durable-execution.ts +46 -88
  144. package/src/edge-kit.ts +27 -0
  145. package/src/errors.ts +1 -0
  146. package/src/index.ts +1 -1
  147. package/src/internal/durable-approval-protocol.ts +92 -0
  148. package/src/internal/durable-effect-protocol.ts +37 -0
  149. package/src/internal/durable-saga-protocol.ts +55 -0
  150. package/src/internal/route-catalog.ts +6 -0
  151. package/src/internal/route-execution.ts +23 -16
  152. package/src/response-observer.ts +64 -0
  153. package/src/security/baseline.ts +172 -0
  154. package/src/server/bare-error-lane.ts +76 -0
  155. package/src/server/body.ts +8 -0
  156. package/src/server/bridge-markers.ts +24 -0
  157. package/src/server/context.ts +6 -1
  158. package/src/server/header-case.ts +3 -1
  159. package/src/server/http.ts +53 -3
  160. package/src/server/install.ts +14 -0
  161. package/src/server/node-outcome-hook.ts +2 -1
  162. package/src/server/node-outcome.ts +44 -3
  163. package/src/server/plugin.ts +1 -1
  164. package/src/server/query.ts +10 -4
  165. package/src/server/registry.ts +19 -4
  166. package/src/server/request-context.ts +106 -51
  167. package/src/server/respond.ts +26 -1
  168. package/src/server/response-contract-lane.ts +6 -2
  169. package/src/server/response-observer-runtime.ts +135 -0
  170. package/src/server/runtime-core.ts +98 -1
  171. package/src/server/server-types.ts +18 -1
  172. package/src/server/server.ts +607 -376
  173. package/src/server/websocket.ts +127 -17
  174. package/src/server/ws-bun.ts +26 -7
  175. package/src/server.ts +2 -1
  176. package/src/single-copy-register.ts +24 -0
  177. package/src/single-copy.ts +524 -0
@@ -164,10 +164,18 @@ export interface WebSocketHandler<
164
164
  /**
165
165
  * The **outbound** frame contract (server → client), a Standard Schema. Purely type-level: it types
166
166
  * the frames the typed client's `.ws()` handle receives, and documents what this route pushes. The
167
- * server does NOT runtime-validate its own sends - the inbound `messageSchema` guards the trust
168
- * boundary; outbound honesty is the handler author's code, checked by tests.
167
+ * server does NOT runtime-validate its own sends by default - the inbound `messageSchema` guards the
168
+ * trust boundary; outbound honesty is the handler author's code. Set {@link validateSend} to opt into
169
+ * a synchronous runtime check of JSON text/UTF-8 binary frames.
169
170
  */
170
171
  sendSchema?: Send
172
+ /**
173
+ * Opt-in runtime enforcement for {@link sendSchema}. Invalid JSON, schema failures, and async
174
+ * validators are dropped before the native socket's `send()` runs and reported to `error()` when it
175
+ * exists. This is a development/contract check for the server's own output, not an inbound security
176
+ * boundary. It is off by default and requires `sendSchema` at registration.
177
+ */
178
+ validateSend?: boolean
171
179
  /** Decode versioned transport frames before inbound schema validation. Omit for legacy JSON. */
172
180
  transport?: {
173
181
  readonly registry: TransportCodecRegistry
@@ -246,24 +254,19 @@ function toBinary(raw: unknown): Uint8Array {
246
254
  * `open` event (Deno). Lifecycle callbacks are error-isolated - a throw (or async rejection) routes to
247
255
  * `error()` and never tears the process down; binary frames are normalized to `Uint8Array`.
248
256
  */
249
- export function attachWebSocket(
257
+ export function attachWebSocket<
258
+ Data = unknown,
259
+ Env = unknown,
260
+ Schema extends StandardSchemaV1 | undefined = undefined,
261
+ Send extends StandardSchemaV1 | undefined = undefined,
262
+ >(
250
263
  socket: StandardWebSocket,
251
- handler: WebSocketHandler,
264
+ handler: WebSocketHandler<Data, Env, Schema, Send>,
252
265
  data: unknown,
253
266
  options: { openNow: boolean; pubsub: TopicRegistry; maxPayloadBytes?: number },
254
- ): NifraWebSocket {
267
+ ): NifraWebSocket<Data> {
255
268
  const { pubsub } = options
256
- const ws: NifraWebSocket = {
257
- send: (payload) => socket.send(payload),
258
- close: (code, reason) => socket.close(code, reason),
259
- get readyState() {
260
- return socket.readyState
261
- },
262
- subscribe: (topic) => pubsub.subscribe(topic, ws),
263
- unsubscribe: (topic) => pubsub.unsubscribe(topic, ws),
264
- data,
265
- raw: socket,
266
- }
269
+ let ws!: NifraWebSocket<Data>
267
270
  const reportError = (error: unknown): void => {
268
271
  if (handler.error === undefined) return
269
272
  try {
@@ -273,6 +276,24 @@ export function attachWebSocket(
273
276
  /* the error handler itself failed - last resort, swallow */
274
277
  }
275
278
  }
279
+ const send = createWebSocketSender(
280
+ (payload) => socket.send(payload),
281
+ handler.sendSchema,
282
+ handler.validateSend,
283
+ handler.transport,
284
+ reportError,
285
+ )
286
+ ws = {
287
+ send,
288
+ close: (code, reason) => socket.close(code, reason),
289
+ get readyState() {
290
+ return socket.readyState
291
+ },
292
+ subscribe: (topic) => pubsub.subscribe(topic, ws),
293
+ unsubscribe: (topic) => pubsub.unsubscribe(topic, ws),
294
+ data: data as Data,
295
+ raw: socket,
296
+ }
276
297
  const safe = (call: () => MaybePromise<void>): void => {
277
298
  try {
278
299
  const r = call()
@@ -300,7 +321,13 @@ export function attachWebSocket(
300
321
  return
301
322
  }
302
323
  }
303
- safe(() => handler.message?.(ws, payload))
324
+ safe(() =>
325
+ (
326
+ handler.message as
327
+ | ((socket: NifraWebSocket<Data>, value: WebSocketData) => MaybePromise<void>)
328
+ | undefined
329
+ )?.(ws, payload),
330
+ )
304
331
  })
305
332
  socket.addEventListener(
306
333
  "close",
@@ -321,6 +348,89 @@ export function attachWebSocket(
321
348
  const WS_MESSAGE_DECODER = new TextDecoder()
322
349
  const WS_PAYLOAD_ENCODER = new TextEncoder()
323
350
 
351
+ type WebSocketSendPayload = string | ArrayBufferView | ArrayBuffer
352
+
353
+ const OUTBOUND_VALIDATION_ERROR = "outbound WebSocket frame failed sendSchema validation"
354
+
355
+ function outboundValueOf(
356
+ payload: WebSocketSendPayload,
357
+ transport: WebSocketHandler["transport"],
358
+ ): unknown {
359
+ const text =
360
+ typeof payload === "string"
361
+ ? payload
362
+ : WS_MESSAGE_DECODER.decode(
363
+ payload instanceof ArrayBuffer
364
+ ? new Uint8Array(payload)
365
+ : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength),
366
+ )
367
+ return transport === undefined
368
+ ? JSON.parse(text)
369
+ : decodeTransportFrame(text, transport.registry, {
370
+ ...(transport.maxBytes === undefined ? {} : { maxBytes: transport.maxBytes }),
371
+ })
372
+ }
373
+
374
+ /**
375
+ * Build the socket's send function once. The disabled path returns the native sender directly, so
376
+ * the default type-only contract adds no per-frame branch or schema work. Runtime validation is
377
+ * deliberately synchronous: a socket `send()` cannot await an async Standard Schema without changing
378
+ * the portable API, so an async result fails closed and is never sent.
379
+ */
380
+ export function createWebSocketSender(
381
+ send: (payload: WebSocketSendPayload) => void,
382
+ schema: StandardSchemaV1 | undefined,
383
+ enabled: boolean | undefined,
384
+ transport: WebSocketHandler["transport"],
385
+ onError: (error: unknown) => void,
386
+ ): (payload: WebSocketSendPayload) => void {
387
+ if (enabled !== true || schema === undefined) return send
388
+ let reporting = false
389
+ const reportFailure = (error: unknown): void => {
390
+ // An error handler may try to send a diagnostic. Do not recurse forever when that diagnostic also
391
+ // violates the same outbound contract. EVERY failure path reports through here - a path that calls
392
+ // `onError` directly reopens that recursion for the one rejection shape it happens to cover.
393
+ if (reporting) return
394
+ reporting = true
395
+ try {
396
+ onError(error)
397
+ } finally {
398
+ reporting = false
399
+ }
400
+ }
401
+ return (payload) => {
402
+ let value: unknown
403
+ try {
404
+ value = outboundValueOf(payload, transport)
405
+ } catch {
406
+ reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
407
+ return
408
+ }
409
+
410
+ let outcome: ValidationOutcome<unknown> | Promise<ValidationOutcome<unknown>>
411
+ try {
412
+ outcome = validateStandard(schema, value) as
413
+ | ValidationOutcome<unknown>
414
+ | Promise<ValidationOutcome<unknown>>
415
+ } catch {
416
+ reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
417
+ return
418
+ }
419
+ if (outcome instanceof Promise) {
420
+ // Do not await or send later: the portable `send()` contract is synchronous. Attach a rejection
421
+ // handler so a validator that rejects cannot become an unhandled process-level rejection.
422
+ outcome.catch(() => {})
423
+ reportFailure(new Error(`${OUTBOUND_VALIDATION_ERROR}; validator must be synchronous`))
424
+ return
425
+ }
426
+ if (!outcome.ok) {
427
+ reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
428
+ return
429
+ }
430
+ send(payload)
431
+ }
432
+ }
433
+
324
434
  /**
325
435
  * If the handler declares a `messageSchema`, return a copy whose `message` validates each frame -
326
436
  * parse as JSON, run the Standard Schema, then call the user's `message` with the typed value, or
@@ -5,7 +5,13 @@
5
5
  * (erased). No `Bun.*` calls here - the runtime seam stays in `server.ts` (see runtime-boundary.test).
6
6
  */
7
7
 
8
- import type { NifraWebSocket, TopicRegistry, WebSocketData, WebSocketHandler } from "./websocket.ts"
8
+ import {
9
+ createWebSocketSender,
10
+ type NifraWebSocket,
11
+ type TopicRegistry,
12
+ type WebSocketData,
13
+ type WebSocketHandler,
14
+ } from "./websocket.ts"
9
15
  import type { BunWsHandlers } from "./ws-hook.ts"
10
16
 
11
17
  /** Per-connection state Bun's `websocket` callbacks read via `ws.data`: the matched handler, the
@@ -33,11 +39,24 @@ function toBinaryData(message: ArrayBuffer | Uint8Array): Uint8Array {
33
39
  }
34
40
 
35
41
  /** Wrap a Bun `ServerWebSocket` as the portable {@link NifraWebSocket} handed to WS callbacks. */
36
- function wrapBunSocket(raw: BunSocket, topics: TopicRegistry): NifraWebSocket {
37
- const ws: NifraWebSocket = {
38
- send: (data) => {
42
+ function wrapBunSocket(
43
+ raw: BunSocket,
44
+ topics: TopicRegistry,
45
+ handler: WebSocketHandler,
46
+ ): NifraWebSocket {
47
+ let ws!: NifraWebSocket
48
+ const reportError = (error: unknown): void => reportWsError(error, ws, handler)
49
+ const send = createWebSocketSender(
50
+ (data) => {
39
51
  raw.send(data)
40
52
  },
53
+ handler.sendSchema,
54
+ handler.validateSend,
55
+ handler.transport,
56
+ reportError,
57
+ )
58
+ ws = {
59
+ send,
41
60
  close: (code, reason) => raw.close(code, reason),
42
61
  get readyState() {
43
62
  return raw.readyState
@@ -86,19 +105,19 @@ export function createBunWsHandlers(topics: TopicRegistry): BunWsHandlers {
86
105
  return {
87
106
  open: (raw) => {
88
107
  const ws = raw as BunSocket
89
- const nifra = wrapBunSocket(ws, topics)
108
+ const nifra = wrapBunSocket(ws, topics, ws.data.handler)
90
109
  ws.data.nifra = nifra
91
110
  dispatchWsCallback(() => ws.data.handler.open?.(nifra), nifra, ws.data.handler)
92
111
  },
93
112
  message: (raw, message) => {
94
113
  const ws = raw as BunSocket
95
- const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics)
114
+ const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, ws.data.handler)
96
115
  const data: WebSocketData = typeof message === "string" ? message : toBinaryData(message)
97
116
  dispatchWsCallback(() => ws.data.handler.message?.(nifra, data), nifra, ws.data.handler)
98
117
  },
99
118
  close: (raw, code, reason) => {
100
119
  const ws = raw as BunSocket
101
- const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics)
120
+ const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, ws.data.handler)
102
121
  topics.unsubscribeAll(nifra) // drop topic subscriptions so the registry never holds a dead socket
103
122
  dispatchWsCallback(() => ws.data.handler.close?.(nifra, code, reason), nifra, ws.data.handler)
104
123
  },
package/src/server.ts CHANGED
@@ -45,7 +45,7 @@ export {
45
45
  type ScheduledHandler,
46
46
  toFetchHandler,
47
47
  } from "./server/edge.ts"
48
- export { pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
48
+ export { isSameOriginPath, pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
49
49
  export {
50
50
  commonSecretPatterns,
51
51
  jsonLogger,
@@ -71,6 +71,7 @@ export {
71
71
  type VercelHandler,
72
72
  } from "./server/platform-adapters.ts"
73
73
  export type { Registry, RouteInfo } from "./server/registry.ts"
74
+ export { type PlainRender, type ResponseResult, status } from "./server/runtime-core.ts"
74
75
  export {
75
76
  type AdmissionController,
76
77
  type AdmissionDecision,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Side-effect entry (`@nifrajs/core/single-copy/register`): install the single-copy resolver into the
3
+ * Bun runtime.
4
+ *
5
+ * Preload it - never import it from application code:
6
+ *
7
+ * ```toml
8
+ * # bunfig.toml
9
+ * preload = ["@nifrajs/core/single-copy/register"]
10
+ * [test]
11
+ * preload = ["@nifrajs/core/single-copy/register"]
12
+ * ```
13
+ *
14
+ * A resolver only affects imports that have not resolved yet, and a module's own imports resolve
15
+ * before its body runs. Registering from inside application code would therefore miss exactly the
16
+ * imports it exists to redirect. `preload` runs before the entry point, which is the only moment that
17
+ * covers the whole graph.
18
+ *
19
+ * `nifra check` reads this specifier out of `bunfig.toml` as the proof that the runtime arm is armed,
20
+ * so the string above is a contract rather than a suggestion.
21
+ */
22
+ import { registerSingleCopy } from "./single-copy"
23
+
24
+ registerSingleCopy()