@nifrajs/core 3.1.0 → 3.3.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/README.md +21 -0
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +26 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-approval.d.ts +56 -0
- package/dist/durable-approval.d.ts.map +1 -0
- package/dist/durable-approval.js +233 -0
- package/dist/durable-approval.js.map +1 -0
- package/dist/durable-effect.d.ts +42 -0
- package/dist/durable-effect.d.ts.map +1 -0
- package/dist/durable-effect.js +234 -0
- package/dist/durable-effect.js.map +1 -0
- package/dist/durable-execution.d.ts +8 -346
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +7 -1150
- package/dist/durable-execution.js.map +1 -1
- package/dist/durable-saga.d.ts +94 -0
- package/dist/durable-saga.d.ts.map +1 -0
- package/dist/durable-saga.js +550 -0
- package/dist/durable-saga.js.map +1 -0
- package/dist/edge-kit.d.ts +2 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-approval-protocol.d.ts +1 -1
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
- package/dist/internal/durable-effect-protocol.d.ts +1 -1
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.d.ts +1 -1
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-shared.d.ts +26 -0
- package/dist/internal/durable-shared.d.ts.map +1 -0
- package/dist/internal/durable-shared.js +131 -0
- package/dist/internal/durable-shared.js.map +1 -0
- package/dist/internal/durable-types.d.ts +159 -0
- package/dist/internal/durable-types.d.ts.map +1 -0
- package/dist/internal/durable-types.js +2 -0
- package/dist/internal/durable-types.js.map +1 -0
- package/dist/internal/route-compiler.d.ts +61 -0
- package/dist/internal/route-compiler.d.ts.map +1 -0
- package/dist/internal/route-compiler.js +121 -0
- package/dist/internal/route-compiler.js.map +1 -0
- package/dist/internal/route-execution.d.ts +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +2 -17
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +35 -0
- package/dist/internal/route-lanes.d.ts.map +1 -0
- package/dist/internal/route-lanes.js +150 -0
- package/dist/internal/route-lanes.js.map +1 -0
- package/dist/internal/route-program.d.ts +67 -0
- package/dist/internal/route-program.d.ts.map +1 -0
- package/dist/internal/route-program.js +199 -0
- package/dist/internal/route-program.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +1 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +3 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/body-lane.d.ts +13 -0
- package/dist/server/body-lane.d.ts.map +1 -0
- package/dist/server/body-lane.js +134 -0
- package/dist/server/body-lane.js.map +1 -0
- package/dist/server/contract.d.ts +40 -14
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +7 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +16 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/plugin.d.ts +2 -2
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +6 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +4 -28
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +10 -185
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +39 -2
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime-core.d.ts +18 -2
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +0 -23
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +113 -84
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +506 -639
- package/dist/server/server.js.map +1 -1
- package/dist/server/validation.d.ts +5 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +13 -0
- package/dist/server/validation.js.map +1 -0
- package/dist/server/ws-bun.d.ts +11 -3
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -12
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +3 -2
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +3 -1
- package/dist/single-copy.js.map +1 -1
- package/package.json +6 -3
- package/src/durable-adapters.ts +20 -0
- package/src/durable-approval.ts +281 -0
- package/src/durable-effect.ts +300 -0
- package/src/durable-execution.ts +8 -1627
- package/src/durable-saga.ts +744 -0
- package/src/edge-kit.ts +2 -2
- package/src/index.ts +129 -2
- package/src/internal/durable-approval-protocol.ts +1 -1
- package/src/internal/durable-effect-protocol.ts +1 -1
- package/src/internal/durable-saga-protocol.ts +1 -1
- package/src/internal/durable-shared.ts +157 -0
- package/src/internal/durable-types.ts +194 -0
- package/src/internal/route-compiler.ts +234 -0
- package/src/internal/route-execution.ts +34 -84
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/server/bare-error-lane.ts +3 -2
- package/src/server/body-lane.ts +203 -0
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/plugin.ts +11 -5
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +7 -262
- package/src/server/respond.ts +45 -2
- package/src/server/runtime-core.ts +28 -2
- package/src/server/server-types.ts +3 -0
- package/src/server/server.ts +1152 -1260
- package/src/server/validation.ts +19 -0
- package/src/server/ws-bun.ts +44 -11
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +7 -2
- package/src/single-copy.ts +3 -1
package/src/server/contract.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { METHODS, type Method } from "../router/router.ts"
|
|
|
5
5
|
import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
|
|
6
6
|
import { assertByteLimit } from "./body.ts"
|
|
7
7
|
import type { Context, IdempotencyConfig, Params, RouteSchema } from "./context.ts"
|
|
8
|
-
import type { EmptyRegistry, OutputOf, Registry } from "./registry.ts"
|
|
8
|
+
import type { EmptyRegistry, OutputOf, Registry, RouteInfoFor } from "./registry.ts"
|
|
9
9
|
import { Server } from "./server.ts"
|
|
10
10
|
|
|
11
|
+
type EmptyResponseMap = NonNullable<unknown>
|
|
12
|
+
|
|
11
13
|
/** An additional (non-success) response a contract operation can document, e.g. a `404`. */
|
|
12
14
|
export interface ResponseDef {
|
|
13
15
|
readonly description?: string
|
|
@@ -88,7 +90,9 @@ type OpErrorBodies<Rs extends Record<string, ResponseDef>> = {
|
|
|
88
90
|
[K in keyof Rs as K extends `2${string}`
|
|
89
91
|
? never
|
|
90
92
|
: K extends `${infer N extends number}`
|
|
91
|
-
?
|
|
93
|
+
? Rs[K] extends { schema: StandardSchemaV1 }
|
|
94
|
+
? N
|
|
95
|
+
: never
|
|
92
96
|
: never]: Rs[K] extends {
|
|
93
97
|
schema: infer S extends StandardSchemaV1
|
|
94
98
|
}
|
|
@@ -106,6 +110,26 @@ type OpErrors<O extends OperationDef> = O extends {
|
|
|
106
110
|
: OpErrorBodies<Rs>
|
|
107
111
|
: unknown
|
|
108
112
|
|
|
113
|
+
/** The schema-bearing status map a contract consumer can use for success and failure narrowing. */
|
|
114
|
+
type OpAdditionalResponseMap<O extends OperationDef> = O extends {
|
|
115
|
+
responses: infer Rs extends Record<string, ResponseDef>
|
|
116
|
+
}
|
|
117
|
+
? {
|
|
118
|
+
[K in keyof Rs as K extends `${infer N extends number}`
|
|
119
|
+
? Rs[K] extends { schema: StandardSchemaV1 }
|
|
120
|
+
? N
|
|
121
|
+
: never
|
|
122
|
+
: never]: Rs[K] extends { schema: infer S extends StandardSchemaV1 }
|
|
123
|
+
? InferOutput<S>
|
|
124
|
+
: never
|
|
125
|
+
}
|
|
126
|
+
: EmptyResponseMap
|
|
127
|
+
|
|
128
|
+
type OpResponseMap<O extends OperationDef> = Omit<OpAdditionalResponseMap<O>, 200> &
|
|
129
|
+
(O extends { response: infer R extends StandardSchemaV1 }
|
|
130
|
+
? { 200: InferOutput<R> }
|
|
131
|
+
: EmptyResponseMap)
|
|
132
|
+
|
|
109
133
|
/**
|
|
110
134
|
* RouteInfo as a *decoupled consumer* sees it from the contract alone: the
|
|
111
135
|
* `output` is the declared `response` schema's type, or `unknown` when none is
|
|
@@ -118,6 +142,7 @@ type RouteInfoForOp<O extends OperationDef> = {
|
|
|
118
142
|
readonly query: OpQuery<O>
|
|
119
143
|
readonly body: OpBody<O>
|
|
120
144
|
readonly output: OpResponse<O>
|
|
145
|
+
readonly responses: OpResponseMap<O>
|
|
121
146
|
// The error union from the op's non-2xx `responses` - so a decoupled contract client sees typed error
|
|
122
147
|
// bodies, just like an inline route's `errors`. `unknown` when the op declares no error responses.
|
|
123
148
|
readonly errors: OpErrors<O>
|
|
@@ -143,6 +168,25 @@ type SchemaForOp<O extends OperationDef> = (O extends { body: infer B extends St
|
|
|
143
168
|
: Record<never, never>) &
|
|
144
169
|
(O extends { params: infer P extends StandardSchemaV1 } ? { params: P } : Record<never, never>)
|
|
145
170
|
|
|
171
|
+
type OpResponseSchemas<O extends OperationDef> = O extends {
|
|
172
|
+
responses: infer Rs extends Record<string, ResponseDef>
|
|
173
|
+
}
|
|
174
|
+
? {
|
|
175
|
+
[K in keyof Rs as K extends `2${string}`
|
|
176
|
+
? never
|
|
177
|
+
: Rs[K] extends { schema: StandardSchemaV1 }
|
|
178
|
+
? K extends `${infer N extends number}`
|
|
179
|
+
? N
|
|
180
|
+
: never
|
|
181
|
+
: never]: Rs[K] extends { schema: infer S extends StandardSchemaV1 } ? S : never
|
|
182
|
+
}
|
|
183
|
+
: EmptyResponseMap
|
|
184
|
+
|
|
185
|
+
type RouteSchemaForOp<O extends OperationDef> = SchemaForOp<O> &
|
|
186
|
+
RouteSchema &
|
|
187
|
+
(O extends { response: infer R extends StandardSchemaV1 } ? { response: R } : EmptyResponseMap) &
|
|
188
|
+
(keyof OpResponseSchemas<O> extends never ? EmptyResponseMap : { errors: OpResponseSchemas<O> })
|
|
189
|
+
|
|
146
190
|
/**
|
|
147
191
|
* The handler context for an op - identical to the inline `Context<Path, S>`, so a
|
|
148
192
|
* handler written for an inline route type-checks unchanged under `implement`
|
|
@@ -245,23 +289,12 @@ export type RegistryFromImpl<
|
|
|
245
289
|
C extends ContractShape,
|
|
246
290
|
H extends HandlersFor<C, Ctx>,
|
|
247
291
|
Ctx = NonNullable<unknown>,
|
|
292
|
+
HookOutput = never,
|
|
248
293
|
> = {
|
|
249
294
|
[P in C[keyof C]["path"]]: {
|
|
250
|
-
[K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
readonly body: OpBody<C[K]>
|
|
254
|
-
readonly output: C[K] extends { response: infer R extends StandardSchemaV1 }
|
|
255
|
-
? InferOutput<R>
|
|
256
|
-
: H[K] extends AnyFn
|
|
257
|
-
? OutputOf<H[K]>
|
|
258
|
-
: unknown
|
|
259
|
-
// The error union from the op's non-2xx `responses` (see OpErrors) - a graduated contract client sees
|
|
260
|
-
// the same typed error bodies as the inline `errors` path.
|
|
261
|
-
readonly errors: OpErrors<C[K]>
|
|
262
|
-
// Mirrors RouteInfoForOp: no SSE contract on contract ops (yet) - mode conformance holds.
|
|
263
|
-
readonly sse: never
|
|
264
|
-
}
|
|
295
|
+
[K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: H[K] extends AnyFn
|
|
296
|
+
? RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, OutputOf<H[K]>, HookOutput>
|
|
297
|
+
: RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, unknown, HookOutput>
|
|
265
298
|
}
|
|
266
299
|
}
|
|
267
300
|
|
|
@@ -290,9 +323,24 @@ export function implement<
|
|
|
290
323
|
H extends HandlersFor<C, Ctx>,
|
|
291
324
|
R extends Registry = EmptyRegistry,
|
|
292
325
|
Ctx = NonNullable<unknown>,
|
|
293
|
-
|
|
294
|
-
|
|
326
|
+
HookOutput = never,
|
|
327
|
+
>(
|
|
328
|
+
contract: C,
|
|
329
|
+
handlers: H,
|
|
330
|
+
app?: Server<R, Ctx, HookOutput>,
|
|
331
|
+
): Server<R & RegistryFromImpl<C, H, Ctx, HookOutput>, Ctx, HookOutput> {
|
|
332
|
+
const target = (app ?? new Server()) as Server<R, Ctx, HookOutput>
|
|
295
333
|
const routes = Object.entries(contract).map(([name, op]) => {
|
|
334
|
+
const errors =
|
|
335
|
+
op.responses === undefined
|
|
336
|
+
? undefined
|
|
337
|
+
: Object.fromEntries(
|
|
338
|
+
Object.entries(op.responses)
|
|
339
|
+
.filter(
|
|
340
|
+
([status, response]) => !status.startsWith("2") && response.schema !== undefined,
|
|
341
|
+
)
|
|
342
|
+
.map(([status, response]) => [Number(status), response.schema!]),
|
|
343
|
+
)
|
|
296
344
|
// body/query are validated at the request boundary; `response` is a type + introspection contract
|
|
297
345
|
// ONLY - never validated at runtime and never read on the request hot path (the lifecycle reads
|
|
298
346
|
// schema.body/query by name; the `bare`/bodyOnly/queryOnly fast-path gates ignore `response`). Built
|
|
@@ -304,6 +352,7 @@ export function implement<
|
|
|
304
352
|
op.body !== undefined ||
|
|
305
353
|
op.query !== undefined ||
|
|
306
354
|
op.response !== undefined ||
|
|
355
|
+
(errors !== undefined && Object.keys(errors).length > 0) ||
|
|
307
356
|
op.capabilities !== undefined ||
|
|
308
357
|
op.idempotency !== undefined ||
|
|
309
358
|
op.bodyLimit !== undefined ||
|
|
@@ -316,6 +365,7 @@ export function implement<
|
|
|
316
365
|
...(op.body !== undefined ? { body: op.body } : {}),
|
|
317
366
|
...(op.query !== undefined ? { query: op.query } : {}),
|
|
318
367
|
...(op.response !== undefined ? { response: op.response } : {}),
|
|
368
|
+
...(errors !== undefined && Object.keys(errors).length > 0 ? { errors } : {}),
|
|
319
369
|
...(op.capabilities !== undefined ? { capabilities: op.capabilities } : {}),
|
|
320
370
|
...(op.idempotency !== undefined ? { idempotency: op.idempotency } : {}),
|
|
321
371
|
...(op.bodyLimit !== undefined ? { bodyLimit: op.bodyLimit } : {}),
|
|
@@ -335,5 +385,5 @@ export function implement<
|
|
|
335
385
|
target.registerBatch(routes)
|
|
336
386
|
// Runtime registered exactly the contract's routes through the inline path; the
|
|
337
387
|
// registry type is computed from the contract inputs + handler return types.
|
|
338
|
-
return target as unknown as Server<R & RegistryFromImpl<C, H, Ctx>, Ctx>
|
|
388
|
+
return target as unknown as Server<R & RegistryFromImpl<C, H, Ctx, HookOutput>, Ctx, HookOutput>
|
|
339
389
|
}
|
package/src/server/cookies.ts
CHANGED
|
@@ -202,11 +202,25 @@ const toBase64Url = (buf: ArrayBuffer): string => {
|
|
|
202
202
|
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
206
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/
|
|
207
|
+
|
|
205
208
|
// Returns `Uint8Array<ArrayBuffer>` (not the generic `Uint8Array<ArrayBufferLike>`) so it satisfies
|
|
206
|
-
// WebCrypto's `BufferSource` parameter under TS 5.7+'s typed-array generics.
|
|
209
|
+
// WebCrypto's `BufferSource` parameter under TS 5.7+'s typed-array generics. Reject non-canonical
|
|
210
|
+
// encodings: base64 decoders otherwise accept alternate final characters whose unused padding bits
|
|
211
|
+
// decode to the same bytes, which lets a visibly changed signed cookie verify as if it were original.
|
|
207
212
|
const fromBase64Url = (s: string): Uint8Array<ArrayBuffer> | null => {
|
|
213
|
+
if (s.length === 0 || s.length % 4 === 1 || !BASE64URL_RE.test(s)) return null
|
|
214
|
+
const remainder = s.length % 4
|
|
215
|
+
const last = BASE64URL_ALPHABET.indexOf(s[s.length - 1]!)
|
|
216
|
+
// Two or three base64 characters leave four or two unused low bits respectively. The encoder above
|
|
217
|
+
// always emits those bits as zero; accepting anything else creates a second spelling of the same
|
|
218
|
+
// byte sequence.
|
|
219
|
+
if ((remainder === 2 && (last & 0x0f) !== 0) || (remainder === 3 && (last & 0x03) !== 0))
|
|
220
|
+
return null
|
|
208
221
|
try {
|
|
209
|
-
const
|
|
222
|
+
const padded = s + "=".repeat((4 - remainder) % 4)
|
|
223
|
+
const bin = atob(padded.replace(/-/g, "+").replace(/_/g, "/"))
|
|
210
224
|
const bytes = new Uint8Array(bin.length)
|
|
211
225
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
212
226
|
return bytes
|
package/src/server/plugin.ts
CHANGED
|
@@ -23,7 +23,9 @@ export type NifraPlugin<In extends AnyServer = AnyServer, Out extends AnyServer
|
|
|
23
23
|
* server type it receives, preserving the caller's typed registry and context across `.use()` while
|
|
24
24
|
* still allowing the plugin to register runtime hooks or handlers.
|
|
25
25
|
*/
|
|
26
|
-
export type IdentityPlugin = (<
|
|
26
|
+
export type IdentityPlugin<AddedHookOutput = never> = (<R extends Registry, Ctx, HookOutput>(
|
|
27
|
+
app: Server<R, Ctx, HookOutput>,
|
|
28
|
+
) => Server<R, Ctx, HookOutput | AddedHookOutput>) & {
|
|
27
29
|
readonly pluginName?: string
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -142,11 +144,15 @@ export function definePlugin(
|
|
|
142
144
|
* const api = server().get("/a", h).use(audit).get("/b", h) // /a AND /b stay typed
|
|
143
145
|
* ```
|
|
144
146
|
*/
|
|
145
|
-
export function defineIdentityPlugin(
|
|
147
|
+
export function defineIdentityPlugin<AddedHookOutput = never>(
|
|
146
148
|
name: string,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
// The runtime identity plugin may install lifecycle hooks whose richer type is intentionally not
|
|
150
|
+
// threaded through this legacy identity seam. Accept the resulting server here so existing
|
|
151
|
+
// middleware remains source-compatible; direct typed hooks and future typed middleware can use the
|
|
152
|
+
// concrete Server return type without forcing every identity plugin to become response-aware.
|
|
153
|
+
apply: <S extends AnyServer>(app: S) => S | AnyServer,
|
|
154
|
+
): IdentityPlugin<AddedHookOutput> {
|
|
155
|
+
return Object.assign(apply, { pluginName: name }) as IdentityPlugin<AddedHookOutput>
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
package/src/server/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BinaryResponse, RawResponse } from "../binary.ts"
|
|
2
2
|
import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
|
|
3
3
|
import type { Params, RouteSchema } from "./context.ts"
|
|
4
|
+
import type { StatusResponse } from "./runtime-core.ts"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* One route's input/output shape as the **client** will consume it. `query`/`body`
|
|
@@ -17,6 +18,8 @@ export interface RouteInfo {
|
|
|
17
18
|
readonly query: unknown
|
|
18
19
|
readonly body: unknown
|
|
19
20
|
readonly output: unknown
|
|
21
|
+
/** The complete inferred/declared status-keyed response map. */
|
|
22
|
+
readonly responses?: unknown
|
|
20
23
|
/** The route's declared error bodies as a status-keyed record (from `schema.errors`, e.g.
|
|
21
24
|
* `{ 404: NotFound }`); `unknown` when the route declares none. The typed client turns this into
|
|
22
25
|
* a status-discriminated failure union. */
|
|
@@ -48,22 +51,73 @@ type RegistryQuery<S extends RouteSchema> = S extends { query: infer Q extends S
|
|
|
48
51
|
? InferInput<Q>
|
|
49
52
|
: never
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
type StatusCodeOf<Output> =
|
|
55
|
+
Output extends StatusResponse<infer Code, unknown> ? (number extends Code ? never : Code) : never
|
|
56
|
+
|
|
57
|
+
type StatusBodyAt<Output, Code extends number> =
|
|
58
|
+
Output extends StatusResponse<infer Actual, infer Body>
|
|
59
|
+
? Code extends Actual
|
|
60
|
+
? Body
|
|
61
|
+
: never
|
|
62
|
+
: never
|
|
63
|
+
|
|
64
|
+
type IsSuccessCode<Code extends number> = `${Code}` extends `2${string}` ? true : false
|
|
65
|
+
|
|
66
|
+
type EmptyResponseMap = NonNullable<unknown>
|
|
67
|
+
|
|
68
|
+
type StatusBodies<Output, Codes extends number = StatusCodeOf<Output>> = [Codes] extends [never]
|
|
69
|
+
? EmptyResponseMap
|
|
70
|
+
: { [Code in Codes]: StatusBodyAt<Output, Code> }
|
|
71
|
+
|
|
72
|
+
/** A dynamic status code cannot be classified as success or failure; expose an opaque success arm
|
|
73
|
+
* rather than guessing a status or body schema. Explicit route schemas still override this fallback. */
|
|
74
|
+
type DynamicStatusResponses<Output> =
|
|
75
|
+
Output extends StatusResponse<infer Code, unknown>
|
|
76
|
+
? number extends Code
|
|
77
|
+
? { 200: unknown }
|
|
78
|
+
: EmptyResponseMap
|
|
79
|
+
: EmptyResponseMap
|
|
80
|
+
|
|
81
|
+
type InferredResponses<Output> = (Exclude<
|
|
82
|
+
Output,
|
|
83
|
+
StatusResponse<number, unknown> | Response
|
|
84
|
+
> extends never
|
|
85
|
+
? EmptyResponseMap
|
|
86
|
+
: { 200: Exclude<Output, StatusResponse<number, unknown> | Response> }) &
|
|
87
|
+
StatusBodies<Output> &
|
|
88
|
+
DynamicStatusResponses<Output>
|
|
89
|
+
|
|
90
|
+
type ExplicitResponses<S extends RouteSchema> = (S extends {
|
|
54
91
|
response: infer R extends StandardSchemaV1
|
|
55
92
|
}
|
|
56
|
-
? InferOutput<R>
|
|
57
|
-
:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
type
|
|
63
|
-
|
|
93
|
+
? { 200: InferOutput<R> }
|
|
94
|
+
: EmptyResponseMap) &
|
|
95
|
+
(S extends { errors: infer E extends Record<number, StandardSchemaV1> }
|
|
96
|
+
? { [K in keyof E]: E[K] extends StandardSchemaV1 ? InferOutput<E[K]> : never }
|
|
97
|
+
: EmptyResponseMap)
|
|
98
|
+
|
|
99
|
+
type MergeResponseMaps<Inferred, Explicit> = Omit<Inferred, keyof Explicit> & Explicit
|
|
100
|
+
|
|
101
|
+
type ResponseMap<S extends RouteSchema, Output> = MergeResponseMaps<
|
|
102
|
+
InferredResponses<Output>,
|
|
103
|
+
ExplicitResponses<S>
|
|
104
|
+
>
|
|
105
|
+
|
|
106
|
+
/** Public type seam for build-time consumers that need the complete route response map. */
|
|
107
|
+
export type ResponseMapFor<S extends RouteSchema, Output> = ResponseMap<S, Output>
|
|
108
|
+
|
|
109
|
+
type ResponseErrors<M> = {
|
|
110
|
+
[K in keyof M as K extends number ? (IsSuccessCode<K> extends true ? never : K) : never]: M[K]
|
|
64
111
|
}
|
|
65
|
-
|
|
66
|
-
|
|
112
|
+
|
|
113
|
+
type ResponseSuccessBodies<M> = {
|
|
114
|
+
[K in keyof M]: K extends number ? (IsSuccessCode<K> extends true ? M[K] : never) : never
|
|
115
|
+
}[keyof M]
|
|
116
|
+
|
|
117
|
+
type NonEmptyOrUnknown<M> = keyof M extends never ? unknown : M
|
|
118
|
+
|
|
119
|
+
/** The client-visible output is the union of all inferred/declared 2xx bodies. */
|
|
120
|
+
type RegistryOutput<S extends RouteSchema, Output> = ResponseSuccessBodies<ResponseMap<S, Output>>
|
|
67
121
|
|
|
68
122
|
/** The SSE event payload type from a route's `sse` schema; `never` for ordinary routes. */
|
|
69
123
|
type RegistrySse<S extends RouteSchema> = S extends { sse: infer E extends StandardSchemaV1 }
|
|
@@ -71,12 +125,13 @@ type RegistrySse<S extends RouteSchema> = S extends { sse: infer E extends Stand
|
|
|
71
125
|
: never
|
|
72
126
|
|
|
73
127
|
/** Build a {@link RouteInfo} from a route's path, schema, and handler output type. */
|
|
74
|
-
export type RouteInfoFor<Path extends string, S extends RouteSchema, Output> = {
|
|
128
|
+
export type RouteInfoFor<Path extends string, S extends RouteSchema, Output, HookOutput = never> = {
|
|
75
129
|
readonly params: Params<Path>
|
|
76
130
|
readonly query: RegistryQuery<S>
|
|
77
131
|
readonly body: RegistryBody<S>
|
|
78
|
-
readonly output: RegistryOutput<S, Output>
|
|
79
|
-
readonly
|
|
132
|
+
readonly output: RegistryOutput<S, Output | HookOutput>
|
|
133
|
+
readonly responses?: ResponseMapFor<S, Output | HookOutput>
|
|
134
|
+
readonly errors: NonEmptyOrUnknown<ResponseErrors<ResponseMapFor<S, Output | HookOutput>>>
|
|
80
135
|
readonly sse: RegistrySse<S>
|
|
81
136
|
}
|
|
82
137
|
|
|
@@ -5,20 +5,14 @@
|
|
|
5
5
|
* sits below the server module in the graph.
|
|
6
6
|
*/
|
|
7
7
|
import type { RequestBudget } from "../budget.ts"
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
drainCapped,
|
|
11
|
-
hasTrustedBodyFraming,
|
|
12
|
-
parseContentLength,
|
|
13
|
-
RAW_BODY_READERS,
|
|
14
|
-
readBoundedBytes,
|
|
15
|
-
} from "./body.ts"
|
|
8
|
+
import { applyTransportCap, readBoundedBytes } from "./body.ts"
|
|
9
|
+
import { readBoundedJsonSource } from "./body-lane.ts"
|
|
16
10
|
import type { Platform } from "./context.ts"
|
|
17
11
|
import { type CookieOptions, cookieNamePrefix, parseCookies, serializeCookie } from "./cookies.ts"
|
|
18
12
|
import { headerObjectOf } from "./headers.ts"
|
|
19
13
|
import { plainError } from "./http.ts"
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
14
|
+
import type { ProtoPoisoning } from "./proto-guard.ts"
|
|
15
|
+
import { searchOf } from "./query.ts"
|
|
22
16
|
import { responseJsonContentType } from "./respond.ts"
|
|
23
17
|
import {
|
|
24
18
|
CONTEXT_SEARCH,
|
|
@@ -28,13 +22,11 @@ import {
|
|
|
28
22
|
getUnboundedRequestBudget,
|
|
29
23
|
headerOf,
|
|
30
24
|
isResponseResult,
|
|
31
|
-
PRE_DECODED_BODY,
|
|
32
|
-
type PreDecodedBody,
|
|
33
|
-
type ResponseResult,
|
|
34
25
|
requestOf,
|
|
35
|
-
TEXT_DECODER,
|
|
36
26
|
} from "./runtime-core.ts"
|
|
37
|
-
import type { CtxSet,
|
|
27
|
+
import type { CtxSet, RawContext, RequestSource } from "./server.ts"
|
|
28
|
+
|
|
29
|
+
export { readBodyFramed, readBoundedJsonSource } from "./body-lane.ts"
|
|
38
30
|
|
|
39
31
|
/** A fixed past instant for cookie deletion (`Expires`). A literal epoch - deterministic, unlike an
|
|
40
32
|
* argless `new Date()`. */
|
|
@@ -428,250 +420,3 @@ async function readBoundedJsonBodyOrThrow<T>(
|
|
|
428
420
|
* with an in-cap `Content-Length` take the runtime's fused `json()` and walk the parsed value;
|
|
429
421
|
* chunked or length-less requests fall back to the streaming byte-cap guard.
|
|
430
422
|
*/
|
|
431
|
-
/**
|
|
432
|
-
* Deliberately **not** `async`: the framed lane is the hot path for every JSON POST, and an async
|
|
433
|
-
* wrapper costs a coroutine frame, a wrapper promise, and an extra microtask hop on top of the one
|
|
434
|
-
* the body read already pays (~90ns/request on Node, measured). The lane returns the body read's
|
|
435
|
-
* own promise chain instead, and only the streaming fallback - which is already doing IO in a loop
|
|
436
|
-
* - runs as an async function. Callers that `await` the result are unaffected.
|
|
437
|
-
*/
|
|
438
|
-
type JsonResultContinuation<T> = (result: unknown | ResponseResult) => T | Promise<T>
|
|
439
|
-
type JsonErrorContinuation<T> = (error: unknown) => T | Promise<T>
|
|
440
|
-
type FramedJsonSource = RequestSource & {
|
|
441
|
-
readonly bytes?: () => Promise<Uint8Array>
|
|
442
|
-
readonly jsonWithByteLength?: () => Promise<{
|
|
443
|
-
readonly value: unknown
|
|
444
|
-
readonly byteLength: number
|
|
445
|
-
}>
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export function readBoundedJsonSource(
|
|
449
|
-
req: RequestSource,
|
|
450
|
-
maxBytes: number,
|
|
451
|
-
protoPoisoning?: ProtoPoisoning,
|
|
452
|
-
): Promise<unknown | ResponseResult>
|
|
453
|
-
export function readBoundedJsonSource<T>(
|
|
454
|
-
req: RequestSource,
|
|
455
|
-
maxBytes: number,
|
|
456
|
-
protoPoisoning: ProtoPoisoning,
|
|
457
|
-
onResult: JsonResultContinuation<T>,
|
|
458
|
-
onError: JsonErrorContinuation<T>,
|
|
459
|
-
): Promise<T>
|
|
460
|
-
export function readBoundedJsonSource<T>(
|
|
461
|
-
req: RequestSource,
|
|
462
|
-
maxBytes: number,
|
|
463
|
-
protoPoisoning: ProtoPoisoning = "reject",
|
|
464
|
-
onResult?: JsonResultContinuation<T>,
|
|
465
|
-
onError?: JsonErrorContinuation<T>,
|
|
466
|
-
): Promise<unknown | ResponseResult | T> {
|
|
467
|
-
// A pre-parsing hook (the transport-codec lane) may have decoded the body already - its stash
|
|
468
|
-
// is taken verbatim: the stasher enforced its own byte cap and poisoning policy on text this
|
|
469
|
-
// lane never sees. One symbol read; a miss on ordinary requests costs a cache-line, not a parse.
|
|
470
|
-
const preDecoded = (req as { [PRE_DECODED_BODY]?: PreDecodedBody })[PRE_DECODED_BODY]
|
|
471
|
-
if (preDecoded !== undefined) {
|
|
472
|
-
return onResult === undefined
|
|
473
|
-
? Promise.resolve(preDecoded.value)
|
|
474
|
-
: Promise.resolve(preDecoded.value).then(onResult)
|
|
475
|
-
}
|
|
476
|
-
// Read through the pre-cap readers when the request is transport-capped: this lane enforces its
|
|
477
|
-
// own `maxBytes` (which may exceed the route's transport cap), so the shadowed user-facing
|
|
478
|
-
// readers must not narrow it.
|
|
479
|
-
// Keep the source's header dispatch local: the normal Node source exposes a direct `header`
|
|
480
|
-
// lookup, while a Web Request only exposes `Headers.get`. Rechecking that optional method through
|
|
481
|
-
// `headerOf` twice per body is measurable on the framed lane. The raw-reader shadow is still
|
|
482
|
-
// bypassed for reads, but header authority remains the original source (same semantics as before).
|
|
483
|
-
const raw = ((req as { [RAW_BODY_READERS]?: RequestSource })[RAW_BODY_READERS] ??
|
|
484
|
-
req) as FramedJsonSource
|
|
485
|
-
const sourceHeader = req.header
|
|
486
|
-
const declared =
|
|
487
|
-
sourceHeader === undefined
|
|
488
|
-
? req.headers.get("content-length")
|
|
489
|
-
: sourceHeader.call(req, "content-length")
|
|
490
|
-
if (declared !== null) {
|
|
491
|
-
// A present Content-Length must be a non-negative integer (HTTP grammar: `1*DIGIT`). A
|
|
492
|
-
// non-numeric / negative / fractional / exponential value (`Number()` would happily accept
|
|
493
|
-
// "abc"->NaN, "-5", "1.5", "1e3", "0x10") is malformed -> 400, rather than silently falling
|
|
494
|
-
// through to the streaming guard - which is an UPPER-bound cap only, so a lying SMALLER length
|
|
495
|
-
// would otherwise be read in full. Real HTTP servers only hand us a valid framed length; this
|
|
496
|
-
// hardens hand-built Requests (tests, the in-process client) and crafted input.
|
|
497
|
-
const length = parseContentLength(declared)
|
|
498
|
-
if (length === undefined)
|
|
499
|
-
return onResult === undefined
|
|
500
|
-
? Promise.resolve(plainError(400, "invalid_content_length"))
|
|
501
|
-
: Promise.resolve(plainError(400, "invalid_content_length")).then(onResult)
|
|
502
|
-
if (length > maxBytes)
|
|
503
|
-
return onResult === undefined
|
|
504
|
-
? Promise.resolve(plainError(413, "payload_too_large"))
|
|
505
|
-
: Promise.resolve(plainError(413, "payload_too_large")).then(onResult)
|
|
506
|
-
// Trusted framing (Bun's native route table, Deno.serve): the runtime's HTTP parser already
|
|
507
|
-
// delimited the bytes, so the declared length IS the transport frame and chunked routing is
|
|
508
|
-
// moot - the over-cap 413 above already enforced the byte cap. Take the runtime's fused
|
|
509
|
-
// decode+parse straight away, before the transfer-encoding probe that only exists to route
|
|
510
|
-
// *untrusted* Node/adapter sources to the streaming lane. Skipping that probe drops one
|
|
511
|
-
// `Headers.get()` from every Bun/Deno JSON POST; Node/adapter sources are never marked and fall
|
|
512
|
-
// through below. The parsed-value poisoning walk still applies.
|
|
513
|
-
if (hasTrustedBodyFraming(req)) {
|
|
514
|
-
return raw.json().then(
|
|
515
|
-
(parsed) => {
|
|
516
|
-
let guarded: unknown
|
|
517
|
-
try {
|
|
518
|
-
guarded = guardParsedValue(parsed, protoPoisoning)
|
|
519
|
-
} catch {
|
|
520
|
-
return onResult === undefined
|
|
521
|
-
? plainError(400, "invalid_json")
|
|
522
|
-
: onResult(plainError(400, "invalid_json"))
|
|
523
|
-
}
|
|
524
|
-
return onResult === undefined ? guarded : onResult(guarded)
|
|
525
|
-
},
|
|
526
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
527
|
-
)
|
|
528
|
-
}
|
|
529
|
-
const chunked =
|
|
530
|
-
(sourceHeader === undefined
|
|
531
|
-
? req.headers.get("transfer-encoding")
|
|
532
|
-
: sourceHeader.call(req, "transfer-encoding")) !== null
|
|
533
|
-
if (!chunked) {
|
|
534
|
-
const jsonWithByteLength = raw.jsonWithByteLength
|
|
535
|
-
if (jsonWithByteLength !== undefined) {
|
|
536
|
-
return jsonWithByteLength.call(raw).then(
|
|
537
|
-
({ value, byteLength }) => {
|
|
538
|
-
if (byteLength > length || byteLength > maxBytes) {
|
|
539
|
-
return onResult === undefined
|
|
540
|
-
? plainError(413, "payload_too_large")
|
|
541
|
-
: onResult(plainError(413, "payload_too_large"))
|
|
542
|
-
}
|
|
543
|
-
let guarded: unknown
|
|
544
|
-
try {
|
|
545
|
-
guarded = guardParsedValue(value, protoPoisoning)
|
|
546
|
-
} catch {
|
|
547
|
-
return onResult === undefined
|
|
548
|
-
? plainError(400, "invalid_json")
|
|
549
|
-
: onResult(plainError(400, "invalid_json"))
|
|
550
|
-
}
|
|
551
|
-
return onResult === undefined ? guarded : onResult(guarded)
|
|
552
|
-
},
|
|
553
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
const bytesReader = raw.bytes
|
|
557
|
-
if (bytesReader !== undefined) {
|
|
558
|
-
return bytesReader.call(raw).then(
|
|
559
|
-
(bytes) => {
|
|
560
|
-
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
561
|
-
return onResult === undefined
|
|
562
|
-
? plainError(413, "payload_too_large")
|
|
563
|
-
: onResult(plainError(413, "payload_too_large"))
|
|
564
|
-
}
|
|
565
|
-
try {
|
|
566
|
-
const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
567
|
-
return onResult === undefined ? parsed : onResult(parsed)
|
|
568
|
-
} catch {
|
|
569
|
-
return onResult === undefined
|
|
570
|
-
? plainError(400, "invalid_json")
|
|
571
|
-
: onResult(plainError(400, "invalid_json"))
|
|
572
|
-
}
|
|
573
|
-
},
|
|
574
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
// Read the actual framed bytes before parsing. A declared length is only a hint on Web
|
|
578
|
-
// Requests and adapter-shaped sources: a caller can construct a Request (or an adapter can
|
|
579
|
-
// decode/expand a body) whose header claims fewer bytes than the delivered payload. Calling
|
|
580
|
-
// native `json()` here would accept that understatement and bypass the cap. The byte lane
|
|
581
|
-
// already owns this post-read check; keeping the JSON lane on the same raw-byte read makes the
|
|
582
|
-
// trust boundary consistent. `Uint8Array(buffer)` is a view, not a copy.
|
|
583
|
-
return raw.arrayBuffer().then(
|
|
584
|
-
(buffer) => {
|
|
585
|
-
const bytes = new Uint8Array(buffer)
|
|
586
|
-
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
587
|
-
return onResult === undefined
|
|
588
|
-
? plainError(413, "payload_too_large")
|
|
589
|
-
: onResult(plainError(413, "payload_too_large"))
|
|
590
|
-
}
|
|
591
|
-
let parsed: unknown
|
|
592
|
-
try {
|
|
593
|
-
parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
594
|
-
} catch {
|
|
595
|
-
return onResult === undefined
|
|
596
|
-
? plainError(400, "invalid_json")
|
|
597
|
-
: onResult(plainError(400, "invalid_json"))
|
|
598
|
-
}
|
|
599
|
-
return onResult === undefined ? parsed : onResult(parsed)
|
|
600
|
-
},
|
|
601
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
602
|
-
)
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
const streamed = readStreamedJsonSource(raw, maxBytes, protoPoisoning)
|
|
606
|
-
return onResult === undefined ? streamed : streamed.then(onResult, onError)
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** The shared rejection handler for a body read that never completed (client abort, socket error,
|
|
610
|
-
* malformed framing): the same flat 400 a syntactically invalid body gets. Hoisted so the framed
|
|
611
|
-
* lane allocates no per-request closure for it. */
|
|
612
|
-
const INVALID_JSON = (): ResponseResult => plainError(400, "invalid_json")
|
|
613
|
-
|
|
614
|
-
/** The chunked / length-less fallback: drain under the streaming byte cap, then parse guarded. */
|
|
615
|
-
async function readStreamedJsonSource(
|
|
616
|
-
raw: RequestSource,
|
|
617
|
-
maxBytes: number,
|
|
618
|
-
protoPoisoning: ProtoPoisoning,
|
|
619
|
-
): Promise<unknown | ResponseResult> {
|
|
620
|
-
const body = raw.body
|
|
621
|
-
if (body === null) return plainError(400, "invalid_json")
|
|
622
|
-
const drained = await drainCapped(body, maxBytes)
|
|
623
|
-
if (!drained.ok) return plainError(413, "payload_too_large")
|
|
624
|
-
try {
|
|
625
|
-
return parseJsonGuarded(TEXT_DECODER.decode(drained.bytes), protoPoisoning)
|
|
626
|
-
} catch {
|
|
627
|
-
return plainError(400, "invalid_json")
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* The shared bounded body framing/parser: the single trust-boundary enforcement point every body
|
|
633
|
-
* lane routes through. Content-type dispatch, the urlencoded-form cap, and the JSON path
|
|
634
|
-
* (`Content-Length` pre-reject -> streaming cap -> prototype-poisoning guard, all inside
|
|
635
|
-
* `readBoundedJsonSource`) live here so the fused body runner, the generic body-only lane, and a
|
|
636
|
-
* compact edge entrypoint all enforce the same boundary from one definition. `protoPoisoning` is a
|
|
637
|
-
* parameter, not read off a server instance, so this stays free of any dependency on the class.
|
|
638
|
-
*
|
|
639
|
-
* `onParsed` receives the decoded, guard-screened value; a framing rejection (`Content-Length` over
|
|
640
|
-
* the cap, a lying length caught mid-stream, malformed JSON, an unsupported media type) is rendered
|
|
641
|
-
* through `wrapResponse`; a read that never completed (client abort, socket error) reaches `onError`.
|
|
642
|
-
*/
|
|
643
|
-
export function readBodyFramed<T>(
|
|
644
|
-
source: RequestSource,
|
|
645
|
-
maxBodyBytes: number,
|
|
646
|
-
protoPoisoning: ProtoPoisoning,
|
|
647
|
-
onParsed: (parsed: unknown) => MaybePromise<T>,
|
|
648
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
649
|
-
onError: (err: unknown) => MaybePromise<T>,
|
|
650
|
-
): Promise<T> {
|
|
651
|
-
const contentType = headerOf(source, "content-type") ?? ""
|
|
652
|
-
if (contentType !== "application/json" && !contentType.includes("application/json")) {
|
|
653
|
-
if (isUrlEncodedForm(contentType)) {
|
|
654
|
-
return readBoundedForm(source, maxBodyBytes).then(
|
|
655
|
-
(form) => (isResponseResult(form) ? wrapResponse(form) : onParsed(form)),
|
|
656
|
-
onError,
|
|
657
|
-
) as Promise<T>
|
|
658
|
-
}
|
|
659
|
-
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't fit a
|
|
660
|
-
// value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
661
|
-
return Promise.resolve(wrapResponse(plainError(415, "unsupported_media_type")))
|
|
662
|
-
}
|
|
663
|
-
// All JSON bodies go through readBoundedJsonSource - the single enforcement point for the framed
|
|
664
|
-
// fast path (post-read byte re-check), the streaming cap, and the prototype-poisoning guard. A
|
|
665
|
-
// separate inlined native-json() fast path here would fork the trust boundary in two.
|
|
666
|
-
try {
|
|
667
|
-
return readBoundedJsonSource(
|
|
668
|
-
source,
|
|
669
|
-
maxBodyBytes,
|
|
670
|
-
protoPoisoning,
|
|
671
|
-
(parsed) => (isResponseResult(parsed) ? wrapResponse(parsed) : onParsed(parsed)),
|
|
672
|
-
onError,
|
|
673
|
-
)
|
|
674
|
-
} catch (err) {
|
|
675
|
-
return Promise.resolve(onError(err))
|
|
676
|
-
}
|
|
677
|
-
}
|