@nifrajs/core 5.5.1 → 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.
- package/CHANGELOG.md +153 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +2 -0
- package/dist/evidence.js.map +1 -1
- package/dist/file-type.d.ts +30 -0
- package/dist/file-type.d.ts.map +1 -0
- package/dist/file-type.js +118 -0
- package/dist/file-type.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/route-program.d.ts +11 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +133 -85
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reflection.d.ts +14 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +34 -7
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -0
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js +1 -0
- package/dist/response-observer.js.map +1 -1
- package/dist/server/bare-error-lane.d.ts +4 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +9 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/before-handler.d.ts +10 -0
- package/dist/server/before-handler.d.ts.map +1 -0
- package/dist/server/before-handler.js +12 -0
- package/dist/server/before-handler.js.map +1 -0
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +8 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts.map +1 -1
- package/dist/server/body-parser.js +2 -5
- package/dist/server/body-parser.js.map +1 -1
- package/dist/server/body.d.ts +24 -8
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +86 -33
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +6 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/multipart.d.ts +3 -0
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +226 -97
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +39 -4
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -14
- package/dist/server/query.js.map +1 -1
- package/dist/server/repeated-prefix.d.ts +17 -0
- package/dist/server/repeated-prefix.d.ts.map +1 -0
- package/dist/server/repeated-prefix.js +17 -0
- package/dist/server/repeated-prefix.js.map +1 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +1 -3
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +1 -3
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +21 -16
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +12 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +135 -8
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/runtime-core.d.ts +7 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +8 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -2
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +6 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +239 -78
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +5 -4
- package/dist/server/sse.js.map +1 -1
- package/dist/server/webhook.d.ts +1 -1
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +24 -19
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +18 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +45 -5
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +7 -4
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/tool-cli.d.ts +64 -0
- package/dist/tool-cli.d.ts.map +1 -0
- package/dist/tool-cli.js +495 -0
- package/dist/tool-cli.js.map +1 -0
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/evidence.ts +4 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +2 -1
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +53 -7
- package/src/response-observer.ts +6 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/before-handler.ts +14 -0
- package/src/server/body-lane.ts +19 -9
- package/src/server/body-parser.ts +1 -5
- package/src/server/body.ts +96 -36
- package/src/server/core-copies.ts +1 -1
- package/src/server/http.ts +6 -15
- package/src/server/multipart.ts +232 -100
- package/src/server/proto-guard.ts +39 -4
- package/src/server/query.ts +7 -13
- package/src/server/repeated-prefix.ts +25 -0
- package/src/server/request-context.ts +1 -3
- package/src/server/respond.ts +23 -15
- package/src/server/response-contract-lane.ts +130 -8
- package/src/server/runtime-core.ts +10 -1
- package/src/server/server-types.ts +3 -2
- package/src/server/server.ts +265 -76
- package/src/server/sse.ts +6 -5
- package/src/server/webhook.ts +29 -24
- package/src/server/websocket.ts +57 -8
- package/src/server/ws-bun.ts +7 -3
- package/src/server.ts +1 -0
- package/src/tool-cli.ts +584 -0
package/src/server/respond.ts
CHANGED
|
@@ -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`
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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:
|
|
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
|
-
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
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",
|
|
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":
|
|
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.
|
|
41
|
-
*
|
|
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) =>
|
|
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)
|
|
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
|
-
|
|
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,
|
|
529
|
-
: interpret(settled,
|
|
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"] ??=
|
|
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,8 +91,9 @@ 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`; when Bun's own frame limit refuses it, Bun
|
|
95
|
-
* client sees `1006`, while the `close` handler
|
|
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).
|
|
96
97
|
* Bun and Node stop buffering at the cap, so a huge frame is never held in memory; on Bun a mounted
|
|
97
98
|
* app's smaller cap, and on Deno and Workers every cap, is checked after the runtime has buffered
|
|
98
99
|
* the whole message.
|