@nifrajs/core 5.5.0 → 5.6.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 (138) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/dist/assurance.d.ts.map +1 -1
  3. package/dist/assurance.js.map +1 -1
  4. package/dist/evidence.d.ts +2 -0
  5. package/dist/evidence.d.ts.map +1 -1
  6. package/dist/evidence.js +2 -0
  7. package/dist/evidence.js.map +1 -1
  8. package/dist/file-type.d.ts +30 -0
  9. package/dist/file-type.d.ts.map +1 -0
  10. package/dist/file-type.js +118 -0
  11. package/dist/file-type.js.map +1 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/internal/route-program.d.ts +11 -0
  17. package/dist/internal/route-program.d.ts.map +1 -1
  18. package/dist/internal/route-program.js +133 -85
  19. package/dist/internal/route-program.js.map +1 -1
  20. package/dist/reflection.d.ts +14 -1
  21. package/dist/reflection.d.ts.map +1 -1
  22. package/dist/reflection.js +34 -7
  23. package/dist/reflection.js.map +1 -1
  24. package/dist/response-observer.d.ts +1 -0
  25. package/dist/response-observer.d.ts.map +1 -1
  26. package/dist/response-observer.js +1 -0
  27. package/dist/response-observer.js.map +1 -1
  28. package/dist/server/bare-error-lane.d.ts +4 -1
  29. package/dist/server/bare-error-lane.d.ts.map +1 -1
  30. package/dist/server/bare-error-lane.js +9 -3
  31. package/dist/server/bare-error-lane.js.map +1 -1
  32. package/dist/server/before-handler.d.ts +10 -0
  33. package/dist/server/before-handler.d.ts.map +1 -0
  34. package/dist/server/before-handler.js +12 -0
  35. package/dist/server/before-handler.js.map +1 -0
  36. package/dist/server/body-lane.d.ts.map +1 -1
  37. package/dist/server/body-lane.js +8 -2
  38. package/dist/server/body-lane.js.map +1 -1
  39. package/dist/server/body-parser.d.ts.map +1 -1
  40. package/dist/server/body-parser.js +2 -5
  41. package/dist/server/body-parser.js.map +1 -1
  42. package/dist/server/body.d.ts +24 -8
  43. package/dist/server/body.d.ts.map +1 -1
  44. package/dist/server/body.js +86 -33
  45. package/dist/server/body.js.map +1 -1
  46. package/dist/server/core-copies.js +1 -1
  47. package/dist/server/http.d.ts.map +1 -1
  48. package/dist/server/http.js +6 -17
  49. package/dist/server/http.js.map +1 -1
  50. package/dist/server/multipart.d.ts +3 -0
  51. package/dist/server/multipart.d.ts.map +1 -1
  52. package/dist/server/multipart.js +226 -97
  53. package/dist/server/multipart.js.map +1 -1
  54. package/dist/server/proto-guard.d.ts.map +1 -1
  55. package/dist/server/proto-guard.js +39 -4
  56. package/dist/server/proto-guard.js.map +1 -1
  57. package/dist/server/query.d.ts.map +1 -1
  58. package/dist/server/query.js +8 -14
  59. package/dist/server/query.js.map +1 -1
  60. package/dist/server/repeated-prefix.d.ts +17 -0
  61. package/dist/server/repeated-prefix.d.ts.map +1 -0
  62. package/dist/server/repeated-prefix.js +17 -0
  63. package/dist/server/repeated-prefix.js.map +1 -0
  64. package/dist/server/request-context.d.ts.map +1 -1
  65. package/dist/server/request-context.js +1 -3
  66. package/dist/server/request-context.js.map +1 -1
  67. package/dist/server/respond.d.ts +1 -3
  68. package/dist/server/respond.d.ts.map +1 -1
  69. package/dist/server/respond.js +21 -16
  70. package/dist/server/respond.js.map +1 -1
  71. package/dist/server/response-contract-lane.d.ts +12 -2
  72. package/dist/server/response-contract-lane.d.ts.map +1 -1
  73. package/dist/server/response-contract-lane.js +135 -8
  74. package/dist/server/response-contract-lane.js.map +1 -1
  75. package/dist/server/runtime-core.d.ts +7 -0
  76. package/dist/server/runtime-core.d.ts.map +1 -1
  77. package/dist/server/runtime-core.js +8 -1
  78. package/dist/server/runtime-core.js.map +1 -1
  79. package/dist/server/server-types.d.ts +6 -3
  80. package/dist/server/server-types.d.ts.map +1 -1
  81. package/dist/server/server.d.ts +6 -2
  82. package/dist/server/server.d.ts.map +1 -1
  83. package/dist/server/server.js +239 -78
  84. package/dist/server/server.js.map +1 -1
  85. package/dist/server/sse.d.ts +1 -1
  86. package/dist/server/sse.d.ts.map +1 -1
  87. package/dist/server/sse.js +5 -4
  88. package/dist/server/sse.js.map +1 -1
  89. package/dist/server/webhook.d.ts +1 -1
  90. package/dist/server/webhook.d.ts.map +1 -1
  91. package/dist/server/webhook.js +24 -19
  92. package/dist/server/webhook.js.map +1 -1
  93. package/dist/server/websocket.d.ts +18 -3
  94. package/dist/server/websocket.d.ts.map +1 -1
  95. package/dist/server/websocket.js +45 -5
  96. package/dist/server/websocket.js.map +1 -1
  97. package/dist/server/ws-bun.d.ts.map +1 -1
  98. package/dist/server/ws-bun.js +12 -3
  99. package/dist/server/ws-bun.js.map +1 -1
  100. package/dist/server.d.ts +1 -0
  101. package/dist/server.d.ts.map +1 -1
  102. package/dist/server.js +1 -0
  103. package/dist/server.js.map +1 -1
  104. package/dist/tool-cli.d.ts +64 -0
  105. package/dist/tool-cli.d.ts.map +1 -0
  106. package/dist/tool-cli.js +495 -0
  107. package/dist/tool-cli.js.map +1 -0
  108. package/package.json +11 -1
  109. package/src/assurance.ts +1 -0
  110. package/src/evidence.ts +4 -0
  111. package/src/file-type.ts +130 -0
  112. package/src/index.ts +2 -1
  113. package/src/internal/route-program.ts +261 -93
  114. package/src/reflection.ts +53 -7
  115. package/src/response-observer.ts +6 -0
  116. package/src/server/bare-error-lane.ts +9 -4
  117. package/src/server/before-handler.ts +14 -0
  118. package/src/server/body-lane.ts +19 -9
  119. package/src/server/body-parser.ts +1 -5
  120. package/src/server/body.ts +96 -36
  121. package/src/server/core-copies.ts +1 -1
  122. package/src/server/http.ts +6 -15
  123. package/src/server/multipart.ts +232 -100
  124. package/src/server/proto-guard.ts +39 -4
  125. package/src/server/query.ts +7 -13
  126. package/src/server/repeated-prefix.ts +25 -0
  127. package/src/server/request-context.ts +1 -3
  128. package/src/server/respond.ts +23 -15
  129. package/src/server/response-contract-lane.ts +130 -8
  130. package/src/server/runtime-core.ts +10 -1
  131. package/src/server/server-types.ts +6 -3
  132. package/src/server/server.ts +265 -76
  133. package/src/server/sse.ts +6 -5
  134. package/src/server/webhook.ts +29 -24
  135. package/src/server/websocket.ts +57 -8
  136. package/src/server/ws-bun.ts +17 -2
  137. package/src/server.ts +1 -0
  138. package/src/tool-cli.ts +584 -0
@@ -13,6 +13,7 @@ import {
13
13
  EMPTY_RESPONSE_CONTROLS,
14
14
  type HandlerResult,
15
15
  isResponseResult,
16
+ JSON_CONTENT_TYPE,
16
17
  type PlainRender,
17
18
  plainRenderHeaders,
18
19
  type ResponseResult,
@@ -41,10 +42,9 @@ function plainRenderSet(plain: PlainRender, set: CtxSet): CtxSet {
41
42
  } as CtxSet
42
43
  }
43
44
 
44
- // Keep the fast JSON respond path byte-identical to `Response.json` without probing it at module
45
- // scope: workerd forbids `Response.json()` during startup. A shared `Headers` is safe to reuse
46
- // across responses: the Response constructor copies `init.headers` into its own list.
47
- const JSON_CONTENT_TYPE = "application/json;charset=utf-8"
45
+ // Keep the fast JSON respond path byte-identical to `Response.json` (see `JSON_CONTENT_TYPE`). A
46
+ // shared `Headers` is safe to reuse across responses: the Response constructor copies
47
+ // `init.headers` into its own list.
48
48
  const JSON_CT_HEADERS = new Headers({ "content-type": JSON_CONTENT_TYPE })
49
49
  const JSON_INIT_200: ResponseInit = { status: 200, headers: JSON_CT_HEADERS }
50
50
 
@@ -212,7 +212,7 @@ function denoResponseWithJsonBody(
212
212
  * it exactly as `withJsonContentType` did.
213
213
  */
214
214
  function jsonResponseWithOwnHeaders(
215
- result: object,
215
+ result: unknown,
216
216
  status: number,
217
217
  headers: Record<string, string>,
218
218
  ): Response {
@@ -499,9 +499,16 @@ function tagged(
499
499
  )
500
500
  }
501
501
 
502
- /** Fused-lane respond when `c.set` is untouched. Bun 1.3's native `Response.json` now beats the
503
- * older hand-inlined stringify + Response construction on this lane while preserving the exact
504
- * body/content-type contract; keep the generic fallback for non-JSON values. */
502
+ /** A plain 200 JSON response. On Bun and Deno the native `Response.json` beats a hand-inlined
503
+ * stringify + Response construction; on Node undici's is 18-26% slower than stringify into the
504
+ * prebuilt init. Both keep the exact body/content-type contract. */
505
+ function plainJsonResponse(result: object): Response {
506
+ // `undefined` from stringify (a `toJSON` returning nothing) keeps `Response.json`'s TypeError.
507
+ const body = IS_BUN || IS_DENO ? undefined : (JSON.stringify(result) as string | undefined)
508
+ return body === undefined ? Response.json(result) : new Response(body, JSON_INIT_200)
509
+ }
510
+
511
+ /** Fused-lane respond when `c.set` is untouched; keep the generic fallback for non-JSON values. */
505
512
  export function fusedRespondNoSet(
506
513
  result: unknown,
507
514
  tagResponseBody: ResponseBodyTagOption = false,
@@ -527,7 +534,7 @@ export function fusedRespondNoSet(
527
534
  !(result instanceof Response) &&
528
535
  !isResponseResult(result)
529
536
  ) {
530
- const response = Response.json(result)
537
+ const response = plainJsonResponse(result)
531
538
  return residentBodiesRecorded ? markResidentBody(response) : response
532
539
  }
533
540
  if (
@@ -579,7 +586,7 @@ export function fusedRespondNoSet(
579
586
  }
580
587
  return withBody(
581
588
  statics === undefined
582
- ? Response.json(result)
589
+ ? plainJsonResponse(result)
583
590
  : Response.json(result, statics.responseJsonInit200()),
584
591
  )
585
592
  }
@@ -757,7 +764,7 @@ function renderValue(
757
764
  body,
758
765
  status,
759
766
  headers as Record<string, string>,
760
- JSON_CT_HEADERS.get("content-type") ?? "application/json;charset=utf-8",
767
+ JSON_CONTENT_TYPE,
761
768
  )
762
769
  : // lgtm [js/stack-trace-exposure] the serialized body is not a caught error stack.
763
770
  new Response(body, { status, headers: withJsonContentType(headers) })
@@ -769,8 +776,9 @@ function renderValue(
769
776
  }
770
777
  }
771
778
  if (IS_DENO && headers !== undefined && !(headers instanceof Headers)) {
772
- const body = JSON.stringify(result) as string | undefined
773
- if (body !== undefined) return withBody(denoResponseWithJsonBody(body, status, headers))
779
+ // Deno's own `Response.json` then header writes measured ~10% under stringify, construction and
780
+ // the same writes plus a content-type one; a declared content-type still overrides the default.
781
+ return jsonResponseWithOwnHeaders(result, status, headers)
774
782
  }
775
783
  return withBody(Response.json(result, init))
776
784
  }
@@ -787,12 +795,12 @@ function withJsonContentType(
787
795
  ): Record<string, string> | Headers {
788
796
  if (headers === undefined) return JSON_CT_HEADERS
789
797
  if (headers instanceof Headers) {
790
- if (!headers.has("content-type")) headers.set("content-type", "application/json;charset=utf-8")
798
+ if (!headers.has("content-type")) headers.set("content-type", JSON_CONTENT_TYPE)
791
799
  return headers
792
800
  }
793
801
  return Object.hasOwn(headers, "content-type")
794
802
  ? headers
795
- : { ...headers, "content-type": "application/json;charset=utf-8" }
803
+ : { ...headers, "content-type": JSON_CONTENT_TYPE }
796
804
  }
797
805
 
798
806
  /** Headers that describe a Response's body or its framing: a `c.set` value for one describes a body
@@ -32,13 +32,23 @@
32
32
  * carries its JSON Schema as `jsonSchema` (every `t` schema does) CAN list its declared keys, so for
33
33
  * those the payload is projected onto the declared shape before it is validated.
34
34
  *
35
+ * ## What is judged is what is written
36
+ *
37
+ * `JSON.stringify` writes what a `toJSON` hook returns, not the fields a validator reads off the
38
+ * object. So a value's hooks are applied first, as the serializer would, and that result is validated
39
+ * and sent: a class whose `toJSON` adds a private field fails a closed schema instead of passing on its
40
+ * public ones. `strip` keeps the declared fields of what the hook writes, so a hook that writes `id`
41
+ * from private state keeps it. A `Date` is the exception; it stays a `Date` for a date schema and is
42
+ * written as its ISO string.
43
+ *
35
44
  * ## What enforcement costs
36
45
  *
37
46
  * Less than it looks like it should. With a compiled validator (`@nifrajs/schema`'s `t` compiles at
38
47
  * construction; zod/valibot equivalents are similarly cheap), the check itself measures in the
39
48
  * ~100ns-per-response range - on a realistic middleware-carrying route, enforce mode benchmarks
40
- * within measurement noise of the same route with no contract at all, on Bun and Node alike. The
41
- * one real cost is structural: a contracted route cannot take the bare-route fused lane, because
49
+ * within measurement noise of the same route with no contract at all, on Bun and Node alike. Looking
50
+ * for `toJSON` hooks visits every object in the payload, about 13 ns each, so a 50-row list pays near
51
+ * 0.7 us. The other cost is structural: a contracted route cannot take the bare-route fused lane, because
42
52
  * the check needs the handler's VALUE before it becomes bytes. A route with any middleware, derive,
43
53
  * or lifecycle hook has already left that lane, so for the routes that look like production the
44
54
  * contract is effectively free - declare it.
@@ -166,7 +176,12 @@ function compileProjector(node: unknown): Projector | undefined {
166
176
  const item = Array.isArray(items) ? undefined : compileProjector(items)
167
177
  return item === undefined
168
178
  ? undefined
169
- : (value) => (Array.isArray(value) ? value.map(item) : value)
179
+ : (value) => {
180
+ if (!Array.isArray(value)) return value
181
+ const out: unknown[] = []
182
+ for (let i = 0; i < value.length; i++) out.push(item(writtenAt(value[i], i)))
183
+ return out
184
+ }
170
185
  }
171
186
  if (type !== "object") return undefined
172
187
  const properties = objectField(schema, "properties") ?? {}
@@ -196,7 +211,7 @@ function compileProjector(node: unknown): Projector | undefined {
196
211
  for (const key of closed ? keys : nested.keys()) {
197
212
  if (!hasField(value, key)) continue
198
213
  const projector = nested.get(key)
199
- define(out, key, projector === undefined ? value[key] : projector(value[key]))
214
+ define(out, key, projector === undefined ? value[key] : projector(writtenAt(value[key], key)))
200
215
  }
201
216
  if (patterns.length > 0) projectPatternKeys(value, out, declared, patterns, closed)
202
217
  return out
@@ -340,7 +355,8 @@ function projectPatternKeys(
340
355
  matches++
341
356
  }
342
357
  if (matches === 0) continue
343
- if (matches === 1 && projector !== undefined) define(out, key, projector(value[key]))
358
+ if (matches === 1 && projector !== undefined)
359
+ define(out, key, projector(writtenAt(value[key], key)))
344
360
  else if (closed) define(out, key, value[key])
345
361
  }
346
362
  }
@@ -352,6 +368,101 @@ function define(out: Record<string, unknown>, key: string, value: unknown): void
352
368
  else out[key] = value
353
369
  }
354
370
 
371
+ const DATE_TO_JSON = Date.prototype.toJSON
372
+
373
+ // Deeper than this, the copying walk takes over with its own cycle check.
374
+ const MAX_HOOK_SCAN_DEPTH = 64
375
+
376
+ /** Whether `value` holds a `toJSON` hook other than a `Date`'s. Allocates nothing: most payloads
377
+ * have none, and this is the price every contracted response pays. */
378
+ function hasHook(value: object, depth: number): boolean {
379
+ if (depth > MAX_HOOK_SCAN_DEPTH) return true
380
+ const hook: unknown = Reflect.get(value, "toJSON")
381
+ if (typeof hook === "function") return !(hook === DATE_TO_JSON && value instanceof Date)
382
+ if (Array.isArray(value)) {
383
+ for (let i = 0; i < value.length; i++) {
384
+ const item: unknown = value[i]
385
+ if (typeof item === "object" && item !== null && hasHook(item, depth + 1)) return true
386
+ }
387
+ return false
388
+ }
389
+ // Inherited enumerable keys are scanned too; that only ever sends a value to the copying walk.
390
+ for (const key in value) {
391
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `key` was enumerated from `value`; `Reflect.get` here halves the speed of this for-in.
392
+ const field: unknown = (value as Record<string, unknown>)[key]
393
+ if (typeof field === "object" && field !== null && hasHook(field, depth + 1)) return true
394
+ }
395
+ return false
396
+ }
397
+
398
+ /**
399
+ * What `JSON.stringify` writes for `value`: every `toJSON` hook applied as the serializer applies it,
400
+ * so the contract judges the bytes that go out rather than fields a hook then replaces. Copies only the
401
+ * containers above a hook. A `Date` keeps its object, since its own hook writes nothing but the ISO
402
+ * string a date schema declares.
403
+ */
404
+ const wireValueOf = (value: unknown): unknown =>
405
+ typeof value === "object" && value !== null && hasHook(value, 0)
406
+ ? wireValue(value, "", [])
407
+ : value
408
+
409
+ const wireValue = (value: unknown, key: string | number, path: object[]): unknown =>
410
+ wireChildren(writtenAt(value, key), path)
411
+
412
+ /** `value` as the serializer takes it under `key`: its own hook applied, its fields still as they are. */
413
+ function writtenAt(value: unknown, key: string | number): unknown {
414
+ if (typeof value !== "object" || value === null) return value
415
+ const hook: unknown = Reflect.get(value, "toJSON")
416
+ if (typeof hook !== "function" || (hook === DATE_TO_JSON && value instanceof Date)) return value
417
+ return fieldsOf(Reflect.apply(hook, value, [String(key)]))
418
+ }
419
+
420
+ /**
421
+ * The serializer calls one hook per value, so a hook's result that has a hook of its own is written
422
+ * by its fields. Copying them out keeps that second hook from running when the checked value is sent.
423
+ */
424
+ function fieldsOf(value: unknown): unknown {
425
+ if (typeof value !== "object" || value === null) return value
426
+ if (typeof Reflect.get(value, "toJSON") !== "function") return value
427
+ if (Array.isArray(value)) return Array.from({ length: value.length }, (_, i): unknown => value[i])
428
+ const fields: Record<string, unknown> = {}
429
+ for (const name of Object.keys(value))
430
+ if (name !== "toJSON") define(fields, name, Reflect.get(value, name))
431
+ return fields
432
+ }
433
+
434
+ function wireChildren(value: unknown, path: object[]): unknown {
435
+ // A cycle is left as it is for `JSON.stringify` to refuse.
436
+ if (typeof value !== "object" || value === null || path.includes(value)) return value
437
+ path.push(value)
438
+ if (Array.isArray(value)) {
439
+ let items: unknown[] | undefined
440
+ for (let i = 0; i < value.length; i++) {
441
+ const item: unknown = value[i]
442
+ const written = wireValue(item, i, path)
443
+ if (written !== item) items ??= value.slice(0, i)
444
+ if (items !== undefined) items[i] = written
445
+ }
446
+ path.pop()
447
+ return items ?? value
448
+ }
449
+ const keys = Object.keys(value)
450
+ let out: Record<string, unknown> | undefined
451
+ let index = 0
452
+ for (const name of keys) {
453
+ const field: unknown = Reflect.get(value, name)
454
+ const written = wireValue(field, name, path)
455
+ if (written !== field && out === undefined) {
456
+ out = {}
457
+ for (const earlier of keys.slice(0, index)) define(out, earlier, Reflect.get(value, earlier))
458
+ }
459
+ if (out !== undefined) define(out, name, written)
460
+ index++
461
+ }
462
+ path.pop()
463
+ return out ?? value
464
+ }
465
+
355
466
  const MAX_STRIPPED_NAMES = 20
356
467
  const MAX_NAME_LENGTH = 64
357
468
  const MAX_REPORTED_SETS = 32
@@ -523,10 +634,21 @@ function validateContract(
523
634
  replace: (value: unknown) => unknown,
524
635
  ): ResponseContractOutcome | Promise<ResponseContractOutcome> {
525
636
  const project = policy.strip ? projectorOf(schema) : undefined
526
- const settled = schema["~standard"].validate(project === undefined ? payload : project(payload))
637
+ // Naming what was stripped compares against everything the payload writes, so it walks it whole.
638
+ const before =
639
+ project === undefined || policy.report !== undefined || mode === "warn"
640
+ ? wireValueOf(payload)
641
+ : payload
642
+ // The projection applies the hook of each value it descends into and reads only declared fields,
643
+ // so a quiet strip never enumerates the payload's own keys. What it copies whole is walked after.
644
+ const sent = project === undefined ? before : wireValueOf(project(writtenAt(before, "")))
645
+ const settled = schema["~standard"].validate(sent)
646
+ // When hooks wrote the payload, `warn` sends what they wrote here rather than calling them again,
647
+ // so a hook that answers differently on a second call cannot send what was not judged.
648
+ const shown = mode === "warn" && before !== payload ? replace(before) : served
527
649
  return settled instanceof Promise
528
- ? settled.then((r) => interpret(r, payload, served, mode, replace, policy.report))
529
- : interpret(settled, payload, served, mode, replace, policy.report)
650
+ ? settled.then((r) => interpret(r, before, shown, mode, replace, policy.report))
651
+ : interpret(settled, before, shown, mode, replace, policy.report)
530
652
  }
531
653
 
532
654
  function interpret(
@@ -11,6 +11,15 @@ import type { CtxSet, RequestSource } from "./server.ts"
11
11
  /** A handler returns a `Response` (used as-is) or any value (serialized to JSON). */
12
12
  export type HandlerResult = Response | unknown
13
13
 
14
+ /**
15
+ * The Content-Type the runtime's own `Response.json` writes, which every JSON nifra renders on the Web
16
+ * lanes carries too, so a handler's JSON and the framework's envelopes (built with `Response.json`)
17
+ * agree on one runtime. Bun adds `;charset=utf-8`; Deno, workerd and undici do not. Named rather than
18
+ * probed: workerd forbids `Response.json()` while a module loads.
19
+ */
20
+ export const JSON_CONTENT_TYPE =
21
+ "Bun" in globalThis ? "application/json;charset=utf-8" : "application/json"
22
+
14
23
  export const RESPONSE_RESULT = Symbol.for(NODE_BRIDGE_MARKER_KEYS.responseResult)
15
24
  export const CONTEXT_SET = Symbol("nifra.context.set")
16
25
  export const CONTEXT_SEARCH = Symbol("nifra.context.search")
@@ -178,7 +187,7 @@ export function status(
178
187
  toResponse(): Response {
179
188
  const headers: Record<string, string> = { ...init?.headers }
180
189
  if (body === undefined) return new Response(null, { status: code, headers })
181
- headers["content-type"] ??= "application/json;charset=utf-8"
190
+ headers["content-type"] ??= JSON_CONTENT_TYPE
182
191
  return new Response(JSON.stringify(body), { status: code, headers })
183
192
  },
184
193
  } as StatusResponse<number, unknown>
@@ -91,9 +91,12 @@ export interface ServerOptions {
91
91
  readonly trustBodyFraming?: boolean
92
92
  /**
93
93
  * Max inbound WebSocket message size (bytes). A larger message never reaches the handler and the
94
- * socket closes with `1009`. Default: `maxBodyBytes` (1 MB). Bun and Node stop buffering at the cap,
95
- * so a huge frame is never held in memory; on Bun a mounted app's smaller cap, and on Deno and
96
- * Workers every cap, is checked after the runtime has buffered the whole message.
94
+ * socket closes with `1009`; when Bun's own frame limit refuses it, Bun up to 1.4.3 drops the
95
+ * connection (the client sees `1006`) and Bun 1.4.4 sends `1009`, while the `close` handler gets
96
+ * `1009` either way. Default: `maxBodyBytes` (1 MB).
97
+ * Bun and Node stop buffering at the cap, so a huge frame is never held in memory; on Bun a mounted
98
+ * app's smaller cap, and on Deno and Workers every cap, is checked after the runtime has buffered
99
+ * the whole message.
97
100
  */
98
101
  readonly wsMaxPayloadBytes?: number
99
102
  /** Per-request timeout (ms): a slower request gets a 503 and `ctx.signal` aborts. 0 disables (default). */