@nifrajs/core 3.1.0 → 3.2.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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-compiled general route program.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately a data-and-control-flow compiler, not source generation: user functions and
|
|
5
|
+
* schemas remain opaque references and no request value is ever converted into executable code. The
|
|
6
|
+
* program is frozen at registration so the request path walks one immutable stage sequence instead
|
|
7
|
+
* of rediscovering which lifecycle features are present.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StandardSchemaV1 } from "../schema/standard.ts"
|
|
11
|
+
import type { Registry } from "../server/registry.ts"
|
|
12
|
+
import {
|
|
13
|
+
CONTEXT_SET,
|
|
14
|
+
EMPTY_RESPONSE_CONTROLS,
|
|
15
|
+
isResponseResult,
|
|
16
|
+
type ResponseResult,
|
|
17
|
+
} from "../server/runtime-core.ts"
|
|
18
|
+
import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
|
|
19
|
+
import type {
|
|
20
|
+
InternalHandler,
|
|
21
|
+
RawAfterHandle,
|
|
22
|
+
RawBeforeHandle,
|
|
23
|
+
RawDerive,
|
|
24
|
+
RawErrorHandler,
|
|
25
|
+
RouteEntry,
|
|
26
|
+
} from "./route-execution.ts"
|
|
27
|
+
|
|
28
|
+
export type ProgramValidationKind = "headers" | "params" | "body" | "query"
|
|
29
|
+
|
|
30
|
+
export type RouteProgramStage =
|
|
31
|
+
| { readonly kind: ProgramValidationKind; readonly schema: StandardSchemaV1 }
|
|
32
|
+
| { readonly kind: "derive"; readonly run: RawDerive }
|
|
33
|
+
| { readonly kind: "before"; readonly run: RawBeforeHandle }
|
|
34
|
+
| { readonly kind: "handler"; readonly run: InternalHandler }
|
|
35
|
+
| { readonly kind: "after"; readonly run: RawAfterHandle }
|
|
36
|
+
|
|
37
|
+
/** The complete registration-time lifecycle, in semantic execution order. */
|
|
38
|
+
export interface RouteProgram {
|
|
39
|
+
readonly stages: readonly RouteProgramStage[]
|
|
40
|
+
readonly validationStages: readonly Extract<RouteProgramStage, { kind: ProgramValidationKind }>[]
|
|
41
|
+
readonly derives: readonly RawDerive[]
|
|
42
|
+
readonly beforeHandle: readonly RawBeforeHandle[]
|
|
43
|
+
readonly afterHandle: readonly RawAfterHandle[]
|
|
44
|
+
readonly onError: readonly RawErrorHandler[]
|
|
45
|
+
readonly handler: InternalHandler
|
|
46
|
+
readonly decorations: Record<PropertyKey, unknown>
|
|
47
|
+
readonly hasDecorations: boolean
|
|
48
|
+
readonly bodySchema: StandardSchemaV1 | undefined
|
|
49
|
+
readonly bodyLimit: RouteEntry["bodyLimit"]
|
|
50
|
+
readonly responseContract: RouteEntry["responseContract"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RouteProgramInput {
|
|
54
|
+
readonly schema: RouteEntry["schema"]
|
|
55
|
+
readonly handler: InternalHandler
|
|
56
|
+
readonly derives: readonly RawDerive[]
|
|
57
|
+
readonly beforeHandle: readonly RawBeforeHandle[]
|
|
58
|
+
readonly afterHandle: readonly RawAfterHandle[]
|
|
59
|
+
readonly onError: readonly RawErrorHandler[]
|
|
60
|
+
readonly decorations: Record<PropertyKey, unknown>
|
|
61
|
+
readonly hasDecorations: boolean
|
|
62
|
+
readonly bodySchema: StandardSchemaV1 | undefined
|
|
63
|
+
readonly bodyLimit: RouteEntry["bodyLimit"]
|
|
64
|
+
readonly responseContract: RouteEntry["responseContract"]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build the immutable stage sequence once, while the server is still configurable. */
|
|
68
|
+
export function compileRouteProgram(input: RouteProgramInput): RouteProgram {
|
|
69
|
+
const stages: RouteProgramStage[] = []
|
|
70
|
+
const validationStages: Extract<RouteProgramStage, { kind: ProgramValidationKind }>[] = []
|
|
71
|
+
const schema = input.schema
|
|
72
|
+
if (schema?.headers !== undefined) {
|
|
73
|
+
const stage = { kind: "headers", schema: schema.headers } as const
|
|
74
|
+
stages.push(stage)
|
|
75
|
+
validationStages.push(stage)
|
|
76
|
+
}
|
|
77
|
+
if (schema?.params !== undefined) {
|
|
78
|
+
const stage = { kind: "params", schema: schema.params } as const
|
|
79
|
+
stages.push(stage)
|
|
80
|
+
validationStages.push(stage)
|
|
81
|
+
}
|
|
82
|
+
if (schema?.body !== undefined) {
|
|
83
|
+
const stage = { kind: "body", schema: schema.body } as const
|
|
84
|
+
stages.push(stage)
|
|
85
|
+
validationStages.push(stage)
|
|
86
|
+
}
|
|
87
|
+
if (schema?.query !== undefined) {
|
|
88
|
+
const stage = { kind: "query", schema: schema.query } as const
|
|
89
|
+
stages.push(stage)
|
|
90
|
+
validationStages.push(stage)
|
|
91
|
+
}
|
|
92
|
+
for (const derive of input.derives) stages.push({ kind: "derive", run: derive })
|
|
93
|
+
for (const before of input.beforeHandle) stages.push({ kind: "before", run: before })
|
|
94
|
+
stages.push({ kind: "handler", run: input.handler })
|
|
95
|
+
for (const after of input.afterHandle) stages.push({ kind: "after", run: after })
|
|
96
|
+
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
stages: Object.freeze(stages),
|
|
99
|
+
validationStages: Object.freeze(validationStages),
|
|
100
|
+
derives: Object.freeze([...input.derives]),
|
|
101
|
+
beforeHandle: Object.freeze([...input.beforeHandle]),
|
|
102
|
+
afterHandle: Object.freeze([...input.afterHandle]),
|
|
103
|
+
onError: Object.freeze([...input.onError]),
|
|
104
|
+
handler: input.handler,
|
|
105
|
+
decorations: Object.freeze({ ...input.decorations }),
|
|
106
|
+
hasDecorations: input.hasDecorations,
|
|
107
|
+
bodySchema: input.bodySchema,
|
|
108
|
+
bodyLimit: input.bodyLimit,
|
|
109
|
+
responseContract: input.responseContract,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface RouteProgramRuntime {
|
|
114
|
+
validateProgramStage(
|
|
115
|
+
entry: RouteEntry,
|
|
116
|
+
stage: Extract<RouteProgramStage, { kind: ProgramValidationKind }>,
|
|
117
|
+
source: RequestSource,
|
|
118
|
+
ctx: RawContext,
|
|
119
|
+
): MaybePromise<Response | ResponseResult | undefined>
|
|
120
|
+
readProgramBody(
|
|
121
|
+
entry: RouteEntry,
|
|
122
|
+
source: RequestSource,
|
|
123
|
+
ctx: RawContext,
|
|
124
|
+
): Promise<Response | ResponseResult | undefined>
|
|
125
|
+
handleProgramError<T>(
|
|
126
|
+
entry: RouteEntry,
|
|
127
|
+
error: unknown,
|
|
128
|
+
ctx: RawContext,
|
|
129
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
130
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
131
|
+
): MaybePromise<T>
|
|
132
|
+
finishProgramResult<T>(
|
|
133
|
+
entry: RouteEntry,
|
|
134
|
+
ctx: RawContext,
|
|
135
|
+
result: unknown,
|
|
136
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
137
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
138
|
+
): MaybePromise<T>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function responseSet(ctx: RawContext): CtxSet {
|
|
142
|
+
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isEarly(value: unknown): value is Response | ResponseResult {
|
|
146
|
+
return value instanceof Response || isResponseResult(value)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type ProgramResult<T> = MaybePromise<T>
|
|
150
|
+
|
|
151
|
+
type PendingPhase =
|
|
152
|
+
| { readonly kind: "validation"; readonly next: number }
|
|
153
|
+
| { readonly kind: "derive"; readonly next: number }
|
|
154
|
+
| { readonly kind: "before"; readonly next: number }
|
|
155
|
+
| { readonly kind: "handler" }
|
|
156
|
+
| { readonly kind: "after"; readonly next: number }
|
|
157
|
+
|
|
158
|
+
/** Execute the general program. The first synchronous stage sequence stays synchronous. */
|
|
159
|
+
export function executeRouteProgram<T, R extends Registry, Ctx>(
|
|
160
|
+
runtime: Server<R, Ctx>,
|
|
161
|
+
entry: RouteEntry,
|
|
162
|
+
program: RouteProgram,
|
|
163
|
+
source: RequestSource,
|
|
164
|
+
ctx: RawContext,
|
|
165
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
166
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
167
|
+
): ProgramResult<T> {
|
|
168
|
+
const host = runtime as unknown as RouteProgramRuntime
|
|
169
|
+
try {
|
|
170
|
+
if (program.hasDecorations) Object.assign(ctx, program.decorations)
|
|
171
|
+
return runSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, undefined)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function runSync<T>(
|
|
178
|
+
host: RouteProgramRuntime,
|
|
179
|
+
entry: RouteEntry,
|
|
180
|
+
program: RouteProgram,
|
|
181
|
+
source: RequestSource,
|
|
182
|
+
ctx: RawContext,
|
|
183
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
184
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
185
|
+
validationIndex: number,
|
|
186
|
+
initial: unknown,
|
|
187
|
+
): ProgramResult<T> {
|
|
188
|
+
const validations = program.validationStages
|
|
189
|
+
for (let i = validationIndex; i < validations.length; i++) {
|
|
190
|
+
const stage = validations[i]!
|
|
191
|
+
const outcome =
|
|
192
|
+
stage.kind === "body"
|
|
193
|
+
? host.readProgramBody(entry, source, ctx)
|
|
194
|
+
: host.validateProgramStage(entry, stage, source, ctx)
|
|
195
|
+
if (outcome instanceof Promise) {
|
|
196
|
+
return continueProgram(
|
|
197
|
+
host,
|
|
198
|
+
entry,
|
|
199
|
+
program,
|
|
200
|
+
source,
|
|
201
|
+
ctx,
|
|
202
|
+
finalize,
|
|
203
|
+
wrapResponse,
|
|
204
|
+
{ kind: "validation", next: i + 1 },
|
|
205
|
+
outcome,
|
|
206
|
+
initial,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
if (outcome !== undefined) return wrapResponse(outcome)
|
|
210
|
+
}
|
|
211
|
+
return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0, 0, initial)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runHooksSync<T>(
|
|
215
|
+
host: RouteProgramRuntime,
|
|
216
|
+
entry: RouteEntry,
|
|
217
|
+
program: RouteProgram,
|
|
218
|
+
source: RequestSource,
|
|
219
|
+
ctx: RawContext,
|
|
220
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
221
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
222
|
+
deriveIndex: number,
|
|
223
|
+
beforeIndex: number,
|
|
224
|
+
afterIndex: number,
|
|
225
|
+
initial: unknown,
|
|
226
|
+
): ProgramResult<T> {
|
|
227
|
+
for (let i = deriveIndex; i < program.derives.length; i++) {
|
|
228
|
+
const outcome = program.derives[i]!(ctx)
|
|
229
|
+
if (outcome instanceof Promise) {
|
|
230
|
+
return continueProgram(
|
|
231
|
+
host,
|
|
232
|
+
entry,
|
|
233
|
+
program,
|
|
234
|
+
source,
|
|
235
|
+
ctx,
|
|
236
|
+
finalize,
|
|
237
|
+
wrapResponse,
|
|
238
|
+
{ kind: "derive", next: i + 1 },
|
|
239
|
+
outcome,
|
|
240
|
+
initial,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
if (isEarly(outcome)) return finalize(outcome, responseSet(ctx))
|
|
244
|
+
Object.assign(ctx, outcome)
|
|
245
|
+
}
|
|
246
|
+
for (let i = beforeIndex; i < program.beforeHandle.length; i++) {
|
|
247
|
+
const outcome = program.beforeHandle[i]!(ctx)
|
|
248
|
+
if (outcome instanceof Promise) {
|
|
249
|
+
return continueProgram(
|
|
250
|
+
host,
|
|
251
|
+
entry,
|
|
252
|
+
program,
|
|
253
|
+
source,
|
|
254
|
+
ctx,
|
|
255
|
+
finalize,
|
|
256
|
+
wrapResponse,
|
|
257
|
+
{ kind: "before", next: i + 1 },
|
|
258
|
+
outcome,
|
|
259
|
+
initial,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
if (outcome !== undefined) return finalize(outcome, responseSet(ctx))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const output = program.handler(ctx)
|
|
266
|
+
if (output instanceof Promise) {
|
|
267
|
+
return continueProgram(
|
|
268
|
+
host,
|
|
269
|
+
entry,
|
|
270
|
+
program,
|
|
271
|
+
source,
|
|
272
|
+
ctx,
|
|
273
|
+
finalize,
|
|
274
|
+
wrapResponse,
|
|
275
|
+
{ kind: "handler" },
|
|
276
|
+
output,
|
|
277
|
+
initial,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
let result = output
|
|
281
|
+
for (let i = afterIndex; i < program.afterHandle.length; i++) {
|
|
282
|
+
const outcome = program.afterHandle[i]!(result, ctx)
|
|
283
|
+
if (outcome instanceof Promise) {
|
|
284
|
+
return continueProgram(
|
|
285
|
+
host,
|
|
286
|
+
entry,
|
|
287
|
+
program,
|
|
288
|
+
source,
|
|
289
|
+
ctx,
|
|
290
|
+
finalize,
|
|
291
|
+
wrapResponse,
|
|
292
|
+
{ kind: "after", next: i + 1 },
|
|
293
|
+
outcome,
|
|
294
|
+
result,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
result = outcome
|
|
298
|
+
}
|
|
299
|
+
return host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function continueProgram<T>(
|
|
303
|
+
host: RouteProgramRuntime,
|
|
304
|
+
entry: RouteEntry,
|
|
305
|
+
program: RouteProgram,
|
|
306
|
+
source: RequestSource,
|
|
307
|
+
ctx: RawContext,
|
|
308
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
309
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
310
|
+
pendingPhase: PendingPhase,
|
|
311
|
+
pending: Promise<unknown>,
|
|
312
|
+
initial: unknown,
|
|
313
|
+
): Promise<T> {
|
|
314
|
+
try {
|
|
315
|
+
const settled = await pending
|
|
316
|
+
let result = initial
|
|
317
|
+
if (pendingPhase.kind === "validation") {
|
|
318
|
+
if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
|
|
319
|
+
} else if (pendingPhase.kind === "derive") {
|
|
320
|
+
if (isEarly(settled)) return finalize(settled, responseSet(ctx))
|
|
321
|
+
Object.assign(ctx, settled)
|
|
322
|
+
} else if (pendingPhase.kind === "before") {
|
|
323
|
+
if (settled !== undefined) return finalize(settled, responseSet(ctx))
|
|
324
|
+
} else if (pendingPhase.kind === "handler") {
|
|
325
|
+
result = settled
|
|
326
|
+
} else {
|
|
327
|
+
result = settled
|
|
328
|
+
}
|
|
329
|
+
return await runAsyncStages(
|
|
330
|
+
host,
|
|
331
|
+
entry,
|
|
332
|
+
program,
|
|
333
|
+
source,
|
|
334
|
+
ctx,
|
|
335
|
+
finalize,
|
|
336
|
+
wrapResponse,
|
|
337
|
+
pendingPhase,
|
|
338
|
+
result,
|
|
339
|
+
)
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runAsyncStages<T>(
|
|
346
|
+
host: RouteProgramRuntime,
|
|
347
|
+
entry: RouteEntry,
|
|
348
|
+
program: RouteProgram,
|
|
349
|
+
source: RequestSource,
|
|
350
|
+
ctx: RawContext,
|
|
351
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
352
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
353
|
+
pendingPhase: PendingPhase,
|
|
354
|
+
result: unknown,
|
|
355
|
+
): Promise<T> {
|
|
356
|
+
try {
|
|
357
|
+
if (pendingPhase.kind === "validation") {
|
|
358
|
+
const validations = program.validationStages
|
|
359
|
+
for (let i = pendingPhase.next; i < validations.length; i++) {
|
|
360
|
+
const stage = validations[i]!
|
|
361
|
+
const outcome =
|
|
362
|
+
stage.kind === "body"
|
|
363
|
+
? await host.readProgramBody(entry, source, ctx)
|
|
364
|
+
: host.validateProgramStage(entry, stage, source, ctx)
|
|
365
|
+
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
366
|
+
if (settled !== undefined) return wrapResponse(settled)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const deriveStart = pendingPhase.kind === "derive" ? pendingPhase.next : 0
|
|
371
|
+
if (pendingPhase.kind === "validation" || pendingPhase.kind === "derive") {
|
|
372
|
+
for (let i = deriveStart; i < program.derives.length; i++) {
|
|
373
|
+
const outcome = program.derives[i]!(ctx)
|
|
374
|
+
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
375
|
+
if (isEarly(settled)) return finalize(settled, responseSet(ctx))
|
|
376
|
+
Object.assign(ctx, settled)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const beforeStart = pendingPhase.kind === "before" ? pendingPhase.next : 0
|
|
381
|
+
if (
|
|
382
|
+
pendingPhase.kind === "validation" ||
|
|
383
|
+
pendingPhase.kind === "derive" ||
|
|
384
|
+
pendingPhase.kind === "before"
|
|
385
|
+
) {
|
|
386
|
+
for (let i = beforeStart; i < program.beforeHandle.length; i++) {
|
|
387
|
+
const outcome = program.beforeHandle[i]!(ctx)
|
|
388
|
+
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
389
|
+
if (settled !== undefined) return finalize(settled, responseSet(ctx))
|
|
390
|
+
}
|
|
391
|
+
const output = program.handler(ctx)
|
|
392
|
+
result = output instanceof Promise ? await output : output
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const afterStart = pendingPhase.kind === "after" ? pendingPhase.next : 0
|
|
396
|
+
for (let i = afterStart; i < program.afterHandle.length; i++) {
|
|
397
|
+
const outcome = program.afterHandle[i]!(result, ctx)
|
|
398
|
+
result = outcome instanceof Promise ? await outcome : outcome
|
|
399
|
+
}
|
|
400
|
+
return await host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* which lane caught it. Both the generic bare lane and the fused body runner route here, so that
|
|
18
18
|
* decision lives in exactly one place.
|
|
19
19
|
*/
|
|
20
|
-
import { pathnameOf
|
|
20
|
+
import { pathnameOf } from "./http.ts"
|
|
21
21
|
import type { Logger } from "./logger.ts"
|
|
22
22
|
import { isResponseResult, type ResponseResult } from "./runtime-core.ts"
|
|
23
23
|
import type { CtxSet, RawContext } from "./server.ts"
|
|
@@ -43,11 +43,12 @@ export function renderBareError<T>(
|
|
|
43
43
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
44
44
|
responseSet: (ctx: RawContext) => CtxSet,
|
|
45
45
|
logError: (err: unknown, ctx: RawContext) => void,
|
|
46
|
+
internalError: () => T,
|
|
46
47
|
): T {
|
|
47
48
|
if (err instanceof Response) return wrapResponse(err)
|
|
48
49
|
if (isResponseResult(err)) return finalize(err, responseSet(ctx), ctx)
|
|
49
50
|
logError(err, ctx)
|
|
50
|
-
return
|
|
51
|
+
return internalError()
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared framed body parser. Kept separate from RequestContext so compact edge consumers can
|
|
3
|
+
* reuse the exact trust-boundary lane without bundling the full context implementation (cookies,
|
|
4
|
+
* deferred responses, and request helpers).
|
|
5
|
+
*/
|
|
6
|
+
import { drainCapped, hasTrustedBodyFraming, parseContentLength, RAW_BODY_READERS } from "./body.ts"
|
|
7
|
+
import { plainError } from "./http.ts"
|
|
8
|
+
import { guardParsedValue, type ProtoPoisoning, parseJsonGuarded } from "./proto-guard.ts"
|
|
9
|
+
import { isUrlEncodedForm, readBoundedForm } from "./query.ts"
|
|
10
|
+
import {
|
|
11
|
+
headerOf,
|
|
12
|
+
isResponseResult,
|
|
13
|
+
PRE_DECODED_BODY,
|
|
14
|
+
type PreDecodedBody,
|
|
15
|
+
type ResponseResult,
|
|
16
|
+
TEXT_DECODER,
|
|
17
|
+
} from "./runtime-core.ts"
|
|
18
|
+
import type { MaybePromise, RequestSource } from "./server.ts"
|
|
19
|
+
|
|
20
|
+
type JsonResultContinuation<T> = (result: unknown | ResponseResult) => T | Promise<T>
|
|
21
|
+
type JsonErrorContinuation<T> = (error: unknown) => T | Promise<T>
|
|
22
|
+
|
|
23
|
+
type FramedJsonSource = RequestSource & {
|
|
24
|
+
readonly bytes?: () => Promise<Uint8Array>
|
|
25
|
+
readonly jsonWithByteLength?: () => Promise<{
|
|
26
|
+
readonly value: unknown
|
|
27
|
+
readonly byteLength: number
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const INVALID_JSON = (): ResponseResult => plainError(400, "invalid_json")
|
|
32
|
+
|
|
33
|
+
/** Read a JSON source with the declared-length precheck, delivered-byte recheck, streaming cap, and
|
|
34
|
+
* prototype-poisoning guard in one lane. */
|
|
35
|
+
export function readBoundedJsonSource(
|
|
36
|
+
req: RequestSource,
|
|
37
|
+
maxBytes: number,
|
|
38
|
+
protoPoisoning?: ProtoPoisoning,
|
|
39
|
+
): Promise<unknown | ResponseResult>
|
|
40
|
+
export function readBoundedJsonSource<T>(
|
|
41
|
+
req: RequestSource,
|
|
42
|
+
maxBytes: number,
|
|
43
|
+
protoPoisoning: ProtoPoisoning,
|
|
44
|
+
onResult: JsonResultContinuation<T>,
|
|
45
|
+
onError: JsonErrorContinuation<T>,
|
|
46
|
+
): Promise<T>
|
|
47
|
+
export function readBoundedJsonSource<T>(
|
|
48
|
+
req: RequestSource,
|
|
49
|
+
maxBytes: number,
|
|
50
|
+
protoPoisoning: ProtoPoisoning = "reject",
|
|
51
|
+
onResult?: JsonResultContinuation<T>,
|
|
52
|
+
onError?: JsonErrorContinuation<T>,
|
|
53
|
+
): Promise<unknown | ResponseResult | T> {
|
|
54
|
+
const preDecoded = (req as { [PRE_DECODED_BODY]?: PreDecodedBody })[PRE_DECODED_BODY]
|
|
55
|
+
if (preDecoded !== undefined) {
|
|
56
|
+
return onResult === undefined
|
|
57
|
+
? Promise.resolve(preDecoded.value)
|
|
58
|
+
: Promise.resolve(preDecoded.value).then(onResult)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = ((req as { [RAW_BODY_READERS]?: RequestSource })[RAW_BODY_READERS] ??
|
|
62
|
+
req) as FramedJsonSource
|
|
63
|
+
const declared = headerOf(req, "content-length")
|
|
64
|
+
if (declared !== null) {
|
|
65
|
+
const length = parseContentLength(declared)
|
|
66
|
+
if (length === undefined)
|
|
67
|
+
return onResult === undefined
|
|
68
|
+
? Promise.resolve(plainError(400, "invalid_content_length"))
|
|
69
|
+
: Promise.resolve(plainError(400, "invalid_content_length")).then(onResult)
|
|
70
|
+
if (length > maxBytes)
|
|
71
|
+
return onResult === undefined
|
|
72
|
+
? Promise.resolve(plainError(413, "payload_too_large"))
|
|
73
|
+
: Promise.resolve(plainError(413, "payload_too_large")).then(onResult)
|
|
74
|
+
|
|
75
|
+
if (hasTrustedBodyFraming(req)) {
|
|
76
|
+
return raw.json().then(
|
|
77
|
+
(parsed) => {
|
|
78
|
+
try {
|
|
79
|
+
const guarded = guardParsedValue(parsed, protoPoisoning)
|
|
80
|
+
return onResult === undefined ? guarded : onResult(guarded)
|
|
81
|
+
} catch {
|
|
82
|
+
return onResult === undefined
|
|
83
|
+
? plainError(400, "invalid_json")
|
|
84
|
+
: onResult(plainError(400, "invalid_json"))
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (headerOf(req, "transfer-encoding") === null) {
|
|
92
|
+
const jsonWithByteLength = raw.jsonWithByteLength
|
|
93
|
+
if (jsonWithByteLength !== undefined) {
|
|
94
|
+
return jsonWithByteLength.call(raw).then(
|
|
95
|
+
({ value, byteLength }) => {
|
|
96
|
+
if (byteLength > length || byteLength > maxBytes) {
|
|
97
|
+
return onResult === undefined
|
|
98
|
+
? plainError(413, "payload_too_large")
|
|
99
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const guarded = guardParsedValue(value, protoPoisoning)
|
|
103
|
+
return onResult === undefined ? guarded : onResult(guarded)
|
|
104
|
+
} catch {
|
|
105
|
+
return onResult === undefined
|
|
106
|
+
? plainError(400, "invalid_json")
|
|
107
|
+
: onResult(plainError(400, "invalid_json"))
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bytesReader = raw.bytes
|
|
115
|
+
if (bytesReader !== undefined) {
|
|
116
|
+
return bytesReader.call(raw).then(
|
|
117
|
+
(bytes) => {
|
|
118
|
+
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
119
|
+
return onResult === undefined
|
|
120
|
+
? plainError(413, "payload_too_large")
|
|
121
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
125
|
+
return onResult === undefined ? parsed : onResult(parsed)
|
|
126
|
+
} catch {
|
|
127
|
+
return onResult === undefined
|
|
128
|
+
? plainError(400, "invalid_json")
|
|
129
|
+
: onResult(plainError(400, "invalid_json"))
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return raw.arrayBuffer().then(
|
|
137
|
+
(buffer) => {
|
|
138
|
+
const bytes = new Uint8Array(buffer)
|
|
139
|
+
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
140
|
+
return onResult === undefined
|
|
141
|
+
? plainError(413, "payload_too_large")
|
|
142
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
146
|
+
return onResult === undefined ? parsed : onResult(parsed)
|
|
147
|
+
} catch {
|
|
148
|
+
return onResult === undefined
|
|
149
|
+
? plainError(400, "invalid_json")
|
|
150
|
+
: onResult(plainError(400, "invalid_json"))
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const body = raw.body
|
|
159
|
+
const streamed =
|
|
160
|
+
body === null
|
|
161
|
+
? Promise.resolve<unknown | ResponseResult>(plainError(400, "invalid_json"))
|
|
162
|
+
: drainCapped(body, maxBytes).then((drained) => {
|
|
163
|
+
if (!drained.ok) return plainError(413, "payload_too_large")
|
|
164
|
+
try {
|
|
165
|
+
return parseJsonGuarded(TEXT_DECODER.decode(drained.bytes), protoPoisoning)
|
|
166
|
+
} catch {
|
|
167
|
+
return plainError(400, "invalid_json")
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
return onResult === undefined ? streamed : streamed.then(onResult, onError)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The shared content-type dispatcher around the JSON and urlencoded lanes. */
|
|
174
|
+
export function readBodyFramed<T>(
|
|
175
|
+
source: RequestSource,
|
|
176
|
+
maxBodyBytes: number,
|
|
177
|
+
protoPoisoning: ProtoPoisoning,
|
|
178
|
+
onParsed: (parsed: unknown) => MaybePromise<T>,
|
|
179
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
180
|
+
onError: (err: unknown) => MaybePromise<T>,
|
|
181
|
+
): Promise<T> {
|
|
182
|
+
const contentType = headerOf(source, "content-type") ?? ""
|
|
183
|
+
if (contentType !== "application/json" && !contentType.includes("application/json")) {
|
|
184
|
+
if (isUrlEncodedForm(contentType)) {
|
|
185
|
+
return readBoundedForm(source, maxBodyBytes).then(
|
|
186
|
+
(form) => (isResponseResult(form) ? wrapResponse(form) : onParsed(form)),
|
|
187
|
+
onError,
|
|
188
|
+
) as Promise<T>
|
|
189
|
+
}
|
|
190
|
+
return Promise.resolve(wrapResponse(plainError(415, "unsupported_media_type")))
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return readBoundedJsonSource(
|
|
194
|
+
source,
|
|
195
|
+
maxBodyBytes,
|
|
196
|
+
protoPoisoning,
|
|
197
|
+
(parsed) => (isResponseResult(parsed) ? wrapResponse(parsed) : onParsed(parsed)),
|
|
198
|
+
onError,
|
|
199
|
+
)
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return Promise.resolve(onError(err))
|
|
202
|
+
}
|
|
203
|
+
}
|