@nifrajs/core 5.5.1 → 5.7.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 +246 -0
- package/README.md +5 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/binary.d.ts +15 -1
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -0
- package/dist/binary.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +4 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +4 -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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +2 -0
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +12 -0
- package/dist/internal/hook-origins.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 +19 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +46 -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/answer-step.d.ts +27 -0
- package/dist/server/answer-step.d.ts.map +1 -0
- package/dist/server/answer-step.js +33 -0
- package/dist/server/answer-step.js.map +1 -0
- 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/base64-answers.d.ts +5 -0
- package/dist/server/base64-answers.d.ts.map +1 -0
- package/dist/server/base64-answers.js +132 -0
- package/dist/server/base64-answers.js.map +1 -0
- 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.d.ts.map +1 -1
- package/dist/server/core-copies.js +5 -2
- package/dist/server/core-copies.js.map +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/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/json-response.d.ts +3 -0
- package/dist/server/json-response.d.ts.map +1 -0
- package/dist/server/json-response.js +14 -0
- package/dist/server/json-response.js.map +1 -0
- package/dist/server/json-text.d.ts +16 -0
- package/dist/server/json-text.d.ts.map +1 -0
- package/dist/server/json-text.js +34 -0
- package/dist/server/json-text.js.map +1 -0
- 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/node-outcome.d.ts +5 -0
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +3 -4
- package/dist/server/node-outcome.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 +5 -5
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +3 -4
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +45 -37
- 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 +137 -9
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/route-template.d.ts +10 -0
- package/dist/server/route-template.d.ts.map +1 -0
- package/dist/server/route-template.js +21 -0
- package/dist/server/route-template.js.map +1 -0
- 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 +10 -2
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +17 -8
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +27 -12
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +326 -108
- 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 +7 -5
- package/dist/server/sse.js.map +1 -1
- package/dist/server/text-slots.d.ts +8 -0
- package/dist/server/text-slots.d.ts.map +1 -0
- package/dist/server/text-slots.js +19 -0
- package/dist/server/text-slots.js.map +1 -0
- 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 +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +5 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +26 -17
- package/dist/single-copy.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/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/binary.ts +17 -0
- package/src/edge-kit.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/evidence.ts +8 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +3 -1
- package/src/internal/hook-origins.ts +10 -0
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +71 -7
- package/src/response-observer.ts +6 -0
- package/src/server/answer-step.ts +68 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/base64-answers.ts +138 -0
- 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 +5 -2
- package/src/server/http.ts +6 -15
- package/src/server/install.ts +3 -0
- package/src/server/json-response.ts +13 -0
- package/src/server/json-text.ts +34 -0
- package/src/server/multipart.ts +232 -100
- package/src/server/node-outcome.ts +4 -5
- 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 +5 -5
- package/src/server/respond.ts +47 -30
- package/src/server/response-contract-lane.ts +132 -9
- package/src/server/route-template.ts +24 -0
- package/src/server/runtime-core.ts +12 -2
- package/src/server/server-types.ts +18 -6
- package/src/server/server.ts +361 -112
- package/src/server/sse.ts +8 -6
- package/src/server/text-slots.ts +21 -0
- 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 +2 -0
- package/src/single-copy.ts +31 -15
- package/src/tool-cli.ts +584 -0
- package/src/transport-codec.ts +3 -1
|
@@ -11,8 +11,10 @@ import type { StandardSchemaV1 } from "../schema/standard.ts"
|
|
|
11
11
|
import { markAuthenticatedRequest } from "../server/auth.ts"
|
|
12
12
|
import { declaredHeaderNames } from "../server/headers.ts"
|
|
13
13
|
import { pathnameOf } from "../server/http.ts"
|
|
14
|
+
import type { Logger } from "../server/logger.ts"
|
|
14
15
|
import { queryObjectOf } from "../server/query.ts"
|
|
15
16
|
import type { Registry } from "../server/registry.ts"
|
|
17
|
+
import { toResponse } from "../server/respond.ts"
|
|
16
18
|
import {
|
|
17
19
|
CONTEXT_SEARCH,
|
|
18
20
|
CONTEXT_SET,
|
|
@@ -262,6 +264,77 @@ function authenticationInputOf(
|
|
|
262
264
|
})
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Run the `authenticate()` stages a mount was registered after, in order, against the request the
|
|
269
|
+
* mount would receive: `undefined` once each one authenticated, else the response that refuses it. A
|
|
270
|
+
* stage that throws or decides nothing valid answers 503, and its cause goes to `logger`, as a route
|
|
271
|
+
* error no hook answers does.
|
|
272
|
+
*/
|
|
273
|
+
export function authenticateMount(
|
|
274
|
+
stages: readonly RawAuthStage[],
|
|
275
|
+
request: Request,
|
|
276
|
+
platform: { readonly env?: unknown; readonly clientIp?: string | undefined } | undefined,
|
|
277
|
+
logger: Logger,
|
|
278
|
+
): MaybePromise<Response | undefined> {
|
|
279
|
+
let query: Readonly<Record<string, string | readonly string[]>> | undefined
|
|
280
|
+
const search = (() => {
|
|
281
|
+
const at = request.url.indexOf("?")
|
|
282
|
+
return at === -1 ? "" : request.url.slice(at)
|
|
283
|
+
})()
|
|
284
|
+
const input = Object.freeze({
|
|
285
|
+
method: request.method,
|
|
286
|
+
pathname: pathnameOf(request.url),
|
|
287
|
+
headers: authHeadersOf(request),
|
|
288
|
+
params: Object.freeze({}),
|
|
289
|
+
search,
|
|
290
|
+
get query() {
|
|
291
|
+
query ??= frozenQueryOf(search)
|
|
292
|
+
return query
|
|
293
|
+
},
|
|
294
|
+
request,
|
|
295
|
+
env: platform?.env,
|
|
296
|
+
clientIp: platform?.clientIp,
|
|
297
|
+
signal: request.signal,
|
|
298
|
+
})
|
|
299
|
+
const unavailable = (error: unknown): Response => {
|
|
300
|
+
logger.error("authenticate() stage failed before a mount", {
|
|
301
|
+
method: request.method,
|
|
302
|
+
path: input.pathname,
|
|
303
|
+
error: error instanceof Error ? error.message : String(error),
|
|
304
|
+
})
|
|
305
|
+
return toResponse(defaultAuthenticationResponse("unavailable"), EMPTY_RESPONSE_CONTROLS)
|
|
306
|
+
}
|
|
307
|
+
const decide = (
|
|
308
|
+
index: number,
|
|
309
|
+
result: import("../server/auth.ts").AuthenticationResult<unknown>,
|
|
310
|
+
): MaybePromise<Response | undefined> => {
|
|
311
|
+
if (!isAuthenticationDecision(result)) {
|
|
312
|
+
const id = stages[index]?.id
|
|
313
|
+
return unavailable(new TypeError(`authenticate() stage "${id}" returned an invalid decision`))
|
|
314
|
+
}
|
|
315
|
+
if (result.kind === "rejected") {
|
|
316
|
+
const refusal = result.response ?? defaultAuthenticationResponse(result.reason)
|
|
317
|
+
return refusal instanceof Response ? refusal : toResponse(refusal, EMPTY_RESPONSE_CONTROLS)
|
|
318
|
+
}
|
|
319
|
+
markAuthenticatedRequest(request)
|
|
320
|
+
return run(index + 1)
|
|
321
|
+
}
|
|
322
|
+
const run = (index: number): MaybePromise<Response | undefined> => {
|
|
323
|
+
const stage = stages[index]
|
|
324
|
+
if (stage === undefined) return undefined
|
|
325
|
+
let outcome: MaybePromise<import("../server/auth.ts").AuthenticationResult<unknown>>
|
|
326
|
+
try {
|
|
327
|
+
outcome = stage.run(input)
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return unavailable(error)
|
|
330
|
+
}
|
|
331
|
+
return outcome instanceof Promise
|
|
332
|
+
? outcome.then((settled) => decide(index, settled), unavailable)
|
|
333
|
+
: decide(index, outcome)
|
|
334
|
+
}
|
|
335
|
+
return run(0)
|
|
336
|
+
}
|
|
337
|
+
|
|
265
338
|
function defaultAuthenticationResponse(
|
|
266
339
|
reason: import("../server/auth.ts").AuthenticationFailureReason | undefined,
|
|
267
340
|
): ResponseResult {
|
|
@@ -270,6 +343,25 @@ function defaultAuthenticationResponse(
|
|
|
270
343
|
return status(401, { ok: false, error: "unauthorized" })
|
|
271
344
|
}
|
|
272
345
|
|
|
346
|
+
/** Whether a stage returned a decision it may: a rejection with a known reason and a response, if
|
|
347
|
+
* any, that answers; or a principal. */
|
|
348
|
+
function isAuthenticationDecision(
|
|
349
|
+
result: import("../server/auth.ts").AuthenticationResult<unknown>,
|
|
350
|
+
): boolean {
|
|
351
|
+
return (
|
|
352
|
+
result !== null &&
|
|
353
|
+
typeof result === "object" &&
|
|
354
|
+
(result.kind === "rejected"
|
|
355
|
+
? validAuthenticationFailureReason(result.reason) &&
|
|
356
|
+
(result.response === undefined ||
|
|
357
|
+
result.response instanceof Response ||
|
|
358
|
+
isResponseResult(result.response))
|
|
359
|
+
: result.kind === "authenticated" &&
|
|
360
|
+
result.principal !== null &&
|
|
361
|
+
result.principal !== undefined)
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
273
365
|
function validAuthenticationFailureReason(
|
|
274
366
|
value: unknown,
|
|
275
367
|
): value is import("../server/auth.ts").AuthenticationFailureReason | undefined {
|
|
@@ -410,18 +502,7 @@ function finishAuthentication<T>(
|
|
|
410
502
|
next: number,
|
|
411
503
|
validationComplete = false,
|
|
412
504
|
): ProgramResult<T> {
|
|
413
|
-
if (
|
|
414
|
-
result === null ||
|
|
415
|
-
typeof result !== "object" ||
|
|
416
|
-
(result.kind === "rejected"
|
|
417
|
-
? !validAuthenticationFailureReason(result.reason) ||
|
|
418
|
-
(result.response !== undefined &&
|
|
419
|
-
!(result.response instanceof Response) &&
|
|
420
|
-
!isResponseResult(result.response))
|
|
421
|
-
: result.kind !== "authenticated" ||
|
|
422
|
-
result.principal === null ||
|
|
423
|
-
result.principal === undefined)
|
|
424
|
-
) {
|
|
505
|
+
if (!isAuthenticationDecision(result)) {
|
|
425
506
|
const id = program.authStages[next - 1]!.id
|
|
426
507
|
const error = new TypeError(`authenticate() stage "${id}" returned an invalid decision`)
|
|
427
508
|
return failAuthentication(host, entry, ctx, finalize, wrapResponse, error)
|
|
@@ -499,7 +580,7 @@ async function continueFastAuthenticatedProgram<T>(
|
|
|
499
580
|
try {
|
|
500
581
|
const settled = await pending
|
|
501
582
|
if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
|
|
502
|
-
|
|
583
|
+
const outcome = runFastAuthenticatedProgram(
|
|
503
584
|
host,
|
|
504
585
|
entry,
|
|
505
586
|
program,
|
|
@@ -509,6 +590,7 @@ async function continueFastAuthenticatedProgram<T>(
|
|
|
509
590
|
wrapResponse,
|
|
510
591
|
validationIndex,
|
|
511
592
|
)
|
|
593
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
512
594
|
} catch (error) {
|
|
513
595
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
514
596
|
}
|
|
@@ -585,7 +667,7 @@ async function continueValidationBeforeAuthentication<T>(
|
|
|
585
667
|
try {
|
|
586
668
|
const settled = await pending
|
|
587
669
|
if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
|
|
588
|
-
|
|
670
|
+
const outcome = runValidationBeforeAuthentication(
|
|
589
671
|
host,
|
|
590
672
|
entry,
|
|
591
673
|
program,
|
|
@@ -595,6 +677,7 @@ async function continueValidationBeforeAuthentication<T>(
|
|
|
595
677
|
wrapResponse,
|
|
596
678
|
validationIndex,
|
|
597
679
|
)
|
|
680
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
598
681
|
} catch (error) {
|
|
599
682
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
600
683
|
}
|
|
@@ -616,7 +699,7 @@ function runValidatedProgram<T>(
|
|
|
616
699
|
type ProgramResult<T> = MaybePromise<T>
|
|
617
700
|
|
|
618
701
|
/** Opt-in sensitive-route lane: run authentication/policy hooks before parsing untrusted input. */
|
|
619
|
-
|
|
702
|
+
function runAuthBeforeValidation<T>(
|
|
620
703
|
host: RouteProgramRuntime,
|
|
621
704
|
entry: RouteEntry,
|
|
622
705
|
program: RouteProgram,
|
|
@@ -625,23 +708,53 @@ async function runAuthBeforeValidation<T>(
|
|
|
625
708
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
626
709
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
627
710
|
gated = false,
|
|
628
|
-
|
|
711
|
+
deriveStart = 0,
|
|
712
|
+
beforeStart = 0,
|
|
713
|
+
validationStart = 0,
|
|
714
|
+
): ProgramResult<T> {
|
|
629
715
|
try {
|
|
630
716
|
if (!gated) {
|
|
631
|
-
if (
|
|
717
|
+
if (deriveStart === 0 && beforeStart === 0 && program.hasDecorations)
|
|
718
|
+
Object.assign(ctx, program.decorations)
|
|
632
719
|
|
|
633
|
-
for (
|
|
634
|
-
const result =
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
720
|
+
for (let i = deriveStart; i < program.derives.length; i++) {
|
|
721
|
+
const result = program.derives[i]!(ctx)
|
|
722
|
+
if (result instanceof Promise) {
|
|
723
|
+
return continueProgram(
|
|
724
|
+
host,
|
|
725
|
+
entry,
|
|
726
|
+
program,
|
|
727
|
+
source,
|
|
728
|
+
ctx,
|
|
729
|
+
finalize,
|
|
730
|
+
wrapResponse,
|
|
731
|
+
{ kind: "derive", next: i + 1, policy: true },
|
|
732
|
+
result,
|
|
733
|
+
undefined,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
if (isEarly(result)) return finishEarly(host, entry, ctx, result, finalize, wrapResponse)
|
|
737
|
+
Object.assign(ctx, result)
|
|
638
738
|
}
|
|
639
739
|
|
|
640
|
-
for (
|
|
641
|
-
const result =
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
740
|
+
for (let i = beforeStart; i < program.beforeHandle.length; i++) {
|
|
741
|
+
const result = program.beforeHandle[i]!(ctx)
|
|
742
|
+
if (result instanceof Promise) {
|
|
743
|
+
return continueProgram(
|
|
744
|
+
host,
|
|
745
|
+
entry,
|
|
746
|
+
program,
|
|
747
|
+
source,
|
|
748
|
+
ctx,
|
|
749
|
+
finalize,
|
|
750
|
+
wrapResponse,
|
|
751
|
+
{ kind: "before", next: i + 1, policy: true },
|
|
752
|
+
result,
|
|
753
|
+
undefined,
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
if (result !== undefined)
|
|
757
|
+
return finishEarly(host, entry, ctx, result, finalize, wrapResponse)
|
|
645
758
|
}
|
|
646
759
|
|
|
647
760
|
// The policy hooks have passed, so the idempotency gate may buffer the body now. Not awaited:
|
|
@@ -653,30 +766,52 @@ async function runAuthBeforeValidation<T>(
|
|
|
653
766
|
}
|
|
654
767
|
}
|
|
655
768
|
|
|
656
|
-
for (
|
|
769
|
+
for (let i = validationStart; i < program.validationStages.length; i++) {
|
|
770
|
+
const stage = program.validationStages[i]!
|
|
657
771
|
const result =
|
|
658
772
|
stage.kind === "body"
|
|
659
|
-
?
|
|
773
|
+
? host.readProgramBody(entry, source, ctx)
|
|
660
774
|
: host.validateProgramStage(entry, stage, source, ctx)
|
|
661
|
-
|
|
662
|
-
|
|
775
|
+
if (result instanceof Promise) {
|
|
776
|
+
return continueProgram(
|
|
777
|
+
host,
|
|
778
|
+
entry,
|
|
779
|
+
program,
|
|
780
|
+
source,
|
|
781
|
+
ctx,
|
|
782
|
+
finalize,
|
|
783
|
+
wrapResponse,
|
|
784
|
+
{ kind: "validation", next: i + 1, policy: true },
|
|
785
|
+
result,
|
|
786
|
+
undefined,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
if (result !== undefined) return wrapResponse(result)
|
|
663
790
|
}
|
|
664
791
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
792
|
+
// A pending outcome here already met the error boundary (continueProgram or the response
|
|
793
|
+
// contract), so wrapping it in another catch would only add a promise.
|
|
794
|
+
return runHooksSync(
|
|
795
|
+
host,
|
|
796
|
+
entry,
|
|
797
|
+
program,
|
|
798
|
+
source,
|
|
799
|
+
ctx,
|
|
800
|
+
finalize,
|
|
801
|
+
wrapResponse,
|
|
802
|
+
program.derives.length,
|
|
803
|
+
program.beforeHandle.length,
|
|
804
|
+
undefined,
|
|
805
|
+
)
|
|
671
806
|
} catch (error) {
|
|
672
|
-
return
|
|
807
|
+
return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
673
808
|
}
|
|
674
809
|
}
|
|
675
810
|
|
|
676
811
|
type PendingPhase =
|
|
677
|
-
| { readonly kind: "validation"; readonly next: number }
|
|
678
|
-
| { readonly kind: "derive"; readonly next: number }
|
|
679
|
-
| { readonly kind: "before"; readonly next: number }
|
|
812
|
+
| { readonly kind: "validation"; readonly next: number; readonly policy?: true }
|
|
813
|
+
| { readonly kind: "derive"; readonly next: number; readonly policy?: true }
|
|
814
|
+
| { readonly kind: "before"; readonly next: number; readonly policy?: true }
|
|
680
815
|
| { readonly kind: "handler" }
|
|
681
816
|
| { readonly kind: "after"; readonly next: number }
|
|
682
817
|
|
|
@@ -757,7 +892,7 @@ function runSync<T>(
|
|
|
757
892
|
}
|
|
758
893
|
if (outcome !== undefined) return wrapResponse(outcome)
|
|
759
894
|
}
|
|
760
|
-
return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0,
|
|
895
|
+
return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0, initial)
|
|
761
896
|
}
|
|
762
897
|
|
|
763
898
|
function runHooksSync<T>(
|
|
@@ -770,7 +905,6 @@ function runHooksSync<T>(
|
|
|
770
905
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
771
906
|
deriveIndex: number,
|
|
772
907
|
beforeIndex: number,
|
|
773
|
-
afterIndex: number,
|
|
774
908
|
initial: unknown,
|
|
775
909
|
): ProgramResult<T> {
|
|
776
910
|
for (let i = deriveIndex; i < program.derives.length; i++) {
|
|
@@ -826,7 +960,20 @@ function runHooksSync<T>(
|
|
|
826
960
|
initial,
|
|
827
961
|
)
|
|
828
962
|
}
|
|
829
|
-
|
|
963
|
+
return runAfterSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, output)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function runAfterSync<T>(
|
|
967
|
+
host: RouteProgramRuntime,
|
|
968
|
+
entry: RouteEntry,
|
|
969
|
+
program: RouteProgram,
|
|
970
|
+
source: RequestSource,
|
|
971
|
+
ctx: RawContext,
|
|
972
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
973
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
974
|
+
afterIndex: number,
|
|
975
|
+
result: unknown,
|
|
976
|
+
): ProgramResult<T> {
|
|
830
977
|
for (let i = afterIndex; i < program.afterHandle.length; i++) {
|
|
831
978
|
const outcome = program.afterHandle[i]!(result, ctx)
|
|
832
979
|
if (outcome instanceof Promise) {
|
|
@@ -876,7 +1023,7 @@ async function continueProgram<T>(
|
|
|
876
1023
|
} else {
|
|
877
1024
|
result = settled
|
|
878
1025
|
}
|
|
879
|
-
|
|
1026
|
+
const outcome = runAsyncStages(
|
|
880
1027
|
host,
|
|
881
1028
|
entry,
|
|
882
1029
|
program,
|
|
@@ -887,12 +1034,13 @@ async function continueProgram<T>(
|
|
|
887
1034
|
pendingPhase,
|
|
888
1035
|
result,
|
|
889
1036
|
)
|
|
1037
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
890
1038
|
} catch (error) {
|
|
891
1039
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
892
1040
|
}
|
|
893
1041
|
}
|
|
894
1042
|
|
|
895
|
-
|
|
1043
|
+
function runAsyncStages<T>(
|
|
896
1044
|
host: RouteProgramRuntime,
|
|
897
1045
|
entry: RouteEntry,
|
|
898
1046
|
program: RouteProgram,
|
|
@@ -902,54 +1050,74 @@ async function runAsyncStages<T>(
|
|
|
902
1050
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
903
1051
|
pendingPhase: PendingPhase,
|
|
904
1052
|
result: unknown,
|
|
905
|
-
):
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const outcome = program.derives[i]!(ctx)
|
|
924
|
-
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
925
|
-
if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
926
|
-
Object.assign(ctx, settled)
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const beforeStart = pendingPhase.kind === "before" ? pendingPhase.next : 0
|
|
931
|
-
if (
|
|
932
|
-
pendingPhase.kind === "validation" ||
|
|
933
|
-
pendingPhase.kind === "derive" ||
|
|
934
|
-
pendingPhase.kind === "before"
|
|
935
|
-
) {
|
|
936
|
-
for (let i = beforeStart; i < program.beforeHandle.length; i++) {
|
|
937
|
-
const outcome = program.beforeHandle[i]!(ctx)
|
|
938
|
-
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
939
|
-
if (settled !== undefined)
|
|
940
|
-
return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
941
|
-
}
|
|
942
|
-
const output = program.handler(ctx)
|
|
943
|
-
result = output instanceof Promise ? await output : output
|
|
1053
|
+
): ProgramResult<T> {
|
|
1054
|
+
// After an actual async boundary, resume the same sync runner until the next pending stage.
|
|
1055
|
+
// Sync hooks and finalization need no async function wrapper or additional microtask turn.
|
|
1056
|
+
if (pendingPhase.kind === "validation") {
|
|
1057
|
+
if (pendingPhase.policy) {
|
|
1058
|
+
return runAuthBeforeValidation(
|
|
1059
|
+
host,
|
|
1060
|
+
entry,
|
|
1061
|
+
program,
|
|
1062
|
+
source,
|
|
1063
|
+
ctx,
|
|
1064
|
+
finalize,
|
|
1065
|
+
wrapResponse,
|
|
1066
|
+
true,
|
|
1067
|
+
0,
|
|
1068
|
+
0,
|
|
1069
|
+
pendingPhase.next,
|
|
1070
|
+
)
|
|
944
1071
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1072
|
+
return runSync(
|
|
1073
|
+
host,
|
|
1074
|
+
entry,
|
|
1075
|
+
program,
|
|
1076
|
+
source,
|
|
1077
|
+
ctx,
|
|
1078
|
+
finalize,
|
|
1079
|
+
wrapResponse,
|
|
1080
|
+
pendingPhase.next,
|
|
1081
|
+
result,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
if (pendingPhase.kind === "derive" || pendingPhase.kind === "before") {
|
|
1085
|
+
if (pendingPhase.policy) {
|
|
1086
|
+
return runAuthBeforeValidation(
|
|
1087
|
+
host,
|
|
1088
|
+
entry,
|
|
1089
|
+
program,
|
|
1090
|
+
source,
|
|
1091
|
+
ctx,
|
|
1092
|
+
finalize,
|
|
1093
|
+
wrapResponse,
|
|
1094
|
+
false,
|
|
1095
|
+
pendingPhase.kind === "derive" ? pendingPhase.next : program.derives.length,
|
|
1096
|
+
pendingPhase.kind === "before" ? pendingPhase.next : 0,
|
|
1097
|
+
)
|
|
950
1098
|
}
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
1099
|
+
return runHooksSync(
|
|
1100
|
+
host,
|
|
1101
|
+
entry,
|
|
1102
|
+
program,
|
|
1103
|
+
source,
|
|
1104
|
+
ctx,
|
|
1105
|
+
finalize,
|
|
1106
|
+
wrapResponse,
|
|
1107
|
+
pendingPhase.kind === "derive" ? pendingPhase.next : program.derives.length,
|
|
1108
|
+
pendingPhase.kind === "before" ? pendingPhase.next : 0,
|
|
1109
|
+
result,
|
|
1110
|
+
)
|
|
954
1111
|
}
|
|
1112
|
+
return runAfterSync(
|
|
1113
|
+
host,
|
|
1114
|
+
entry,
|
|
1115
|
+
program,
|
|
1116
|
+
source,
|
|
1117
|
+
ctx,
|
|
1118
|
+
finalize,
|
|
1119
|
+
wrapResponse,
|
|
1120
|
+
pendingPhase.kind === "after" ? pendingPhase.next : 0,
|
|
1121
|
+
result,
|
|
1122
|
+
)
|
|
955
1123
|
}
|
package/src/reflection.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface SchemaReflection {
|
|
|
54
54
|
readonly standard: StandardSchemaV1 | undefined
|
|
55
55
|
/** Raw JSON Schema metadata, when the value exposes it or is itself a raw JSON Schema. */
|
|
56
56
|
readonly jsonSchema: JsonSchema | undefined
|
|
57
|
+
/**
|
|
58
|
+
* Set when `jsonSchema` came from the schema's Standard JSON Schema converter (`~standard.jsonSchema`)
|
|
59
|
+
* rather than from metadata it carries. Such output may use `$defs` and `$ref`.
|
|
60
|
+
*/
|
|
61
|
+
readonly converted?: true
|
|
57
62
|
/** Top-level object fields, or `undefined` when the JSON Schema is absent/non-object. */
|
|
58
63
|
readonly fields: readonly ReflectedSchemaField[] | undefined
|
|
59
64
|
/**
|
|
@@ -61,6 +66,11 @@ export interface SchemaReflection {
|
|
|
61
66
|
* JSON and urlencoded bodies every body schema reads. Absent when it declares none.
|
|
62
67
|
*/
|
|
63
68
|
readonly mediaTypes?: readonly string[]
|
|
69
|
+
/**
|
|
70
|
+
* Set when the schema reads text values with `t.query`'s string coercion (`"1"` and `"TRUE"` are
|
|
71
|
+
* `true`, a lone value is a one-item list), as `t.query`, `t.headers` and `t.cookies` do.
|
|
72
|
+
*/
|
|
73
|
+
readonly coerces?: true
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
export interface ReflectedRouteSchema {
|
|
@@ -83,8 +93,16 @@ export interface ReflectedRouteSchema {
|
|
|
83
93
|
readonly errors?: Readonly<Record<string, SchemaReflection>>
|
|
84
94
|
/** The SSE event-payload schema of a typed streaming route (`app.sse()`). */
|
|
85
95
|
readonly sse?: SchemaReflection
|
|
96
|
+
/**
|
|
97
|
+
* Set when an `onValidationError` hook (the route's own, or the app-wide default) answers this
|
|
98
|
+
* route's validation failures, so its 422 body is whatever the hook returns.
|
|
99
|
+
*/
|
|
100
|
+
readonly validationErrorHook?: true
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/** Which side of a schema to describe: the value a request sends, or the value a response carries. */
|
|
104
|
+
export type SchemaSide = "input" | "output"
|
|
105
|
+
|
|
88
106
|
export interface ReflectedRoute {
|
|
89
107
|
readonly method: string
|
|
90
108
|
readonly path: string
|
|
@@ -118,6 +136,22 @@ const standardOf = (value: unknown): StandardSchemaV1 | undefined => {
|
|
|
118
136
|
: undefined
|
|
119
137
|
}
|
|
120
138
|
|
|
139
|
+
/** The JSON Schema a Standard JSON Schema (`~standard.jsonSchema`) converts `side` to, if it has one. */
|
|
140
|
+
const standardJsonSchemaOf = (
|
|
141
|
+
standard: StandardSchemaV1,
|
|
142
|
+
side: SchemaSide,
|
|
143
|
+
): JsonSchema | undefined => {
|
|
144
|
+
const converters = recordOf(recordOf(standard["~standard"])?.jsonSchema)
|
|
145
|
+
const convert = converters?.[side]
|
|
146
|
+
if (typeof convert !== "function") return undefined
|
|
147
|
+
try {
|
|
148
|
+
return normalizeJsonSchema(Reflect.apply(convert, converters, [{ target: "draft-2020-12" }]))
|
|
149
|
+
} catch {
|
|
150
|
+
// A library throws for a type JSON Schema cannot express (zod's `z.date()`); the schema stays opaque.
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
121
155
|
const jsonSchemaOf = (
|
|
122
156
|
value: unknown,
|
|
123
157
|
standard: StandardSchemaV1 | undefined,
|
|
@@ -194,19 +228,39 @@ const mediaTypesOf = (value: unknown): readonly string[] | undefined => {
|
|
|
194
228
|
}
|
|
195
229
|
}
|
|
196
230
|
|
|
231
|
+
// The mark a coercing `@nifrajs/schema` validate carries.
|
|
232
|
+
const COERCES = Symbol.for("nifra.schema.coerce")
|
|
233
|
+
|
|
234
|
+
const coercesOf = (standard: StandardSchemaV1 | undefined): boolean => {
|
|
235
|
+
try {
|
|
236
|
+
const validate: unknown = standard?.["~standard"].validate
|
|
237
|
+
return typeof validate === "function" && Reflect.get(validate, COERCES) === true
|
|
238
|
+
} catch {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
197
243
|
/**
|
|
198
244
|
* Reflect a Standard Schema, Nifra/TypeBox schema carrier, or raw JSON Schema. Never throws.
|
|
199
245
|
* Validation-only schemas have `standard` but no `jsonSchema`; raw JSON Schema has the reverse.
|
|
246
|
+
* A Standard JSON Schema is read on `side`, since a default or a transform makes the two differ.
|
|
200
247
|
*/
|
|
201
|
-
export function reflectSchema(value: unknown): SchemaReflection {
|
|
248
|
+
export function reflectSchema(value: unknown, side: SchemaSide = "input"): SchemaReflection {
|
|
202
249
|
const standard = standardOf(value)
|
|
203
|
-
const
|
|
250
|
+
const carried = jsonSchemaOf(value, standard)
|
|
251
|
+
const converted =
|
|
252
|
+
carried === undefined && standard !== undefined
|
|
253
|
+
? standardJsonSchemaOf(standard, side)
|
|
254
|
+
: undefined
|
|
255
|
+
const jsonSchema = carried ?? converted
|
|
204
256
|
const mediaTypes = mediaTypesOf(value)
|
|
205
257
|
return {
|
|
206
258
|
standard,
|
|
207
259
|
jsonSchema,
|
|
260
|
+
...(converted !== undefined ? { converted: true as const } : {}),
|
|
208
261
|
fields: fieldsOf(jsonSchema),
|
|
209
262
|
...(mediaTypes !== undefined ? { mediaTypes } : {}),
|
|
263
|
+
...(coercesOf(standard) ? { coerces: true as const } : {}),
|
|
210
264
|
}
|
|
211
265
|
}
|
|
212
266
|
|
|
@@ -222,14 +276,17 @@ const routeCandidates = (source: unknown): readonly unknown[] => {
|
|
|
222
276
|
}
|
|
223
277
|
}
|
|
224
278
|
|
|
225
|
-
const reflectedRouteSchema = (
|
|
279
|
+
const reflectedRouteSchema = (
|
|
280
|
+
value: unknown,
|
|
281
|
+
appValidationHook: boolean,
|
|
282
|
+
): ReflectedRouteSchema | undefined => {
|
|
226
283
|
const schema = recordOf(value)
|
|
227
284
|
if (schema === undefined) return undefined
|
|
228
285
|
const errors = recordOf(schema.errors)
|
|
229
286
|
const reflectedErrors: Record<string, SchemaReflection> = {}
|
|
230
287
|
if (errors !== undefined) {
|
|
231
288
|
for (const [status, errorSchema] of Object.entries(errors)) {
|
|
232
|
-
reflectedErrors[status] = reflectSchema(errorSchema)
|
|
289
|
+
reflectedErrors[status] = reflectSchema(errorSchema, "output")
|
|
233
290
|
}
|
|
234
291
|
}
|
|
235
292
|
return {
|
|
@@ -249,9 +306,14 @@ const reflectedRouteSchema = (value: unknown): ReflectedRouteSchema | undefined
|
|
|
249
306
|
...(schema.body !== undefined ? { body: reflectSchema(schema.body) } : {}),
|
|
250
307
|
...(schema.query !== undefined ? { query: reflectSchema(schema.query) } : {}),
|
|
251
308
|
...(schema.params !== undefined ? { params: reflectSchema(schema.params) } : {}),
|
|
252
|
-
...(schema.response !== undefined
|
|
309
|
+
...(schema.response !== undefined
|
|
310
|
+
? { response: reflectSchema(schema.response, "output") }
|
|
311
|
+
: {}),
|
|
253
312
|
...(Object.keys(reflectedErrors).length > 0 ? { errors: reflectedErrors } : {}),
|
|
254
|
-
...(schema.sse !== undefined ? { sse: reflectSchema(schema.sse) } : {}),
|
|
313
|
+
...(schema.sse !== undefined ? { sse: reflectSchema(schema.sse, "output") } : {}),
|
|
314
|
+
...(appValidationHook || typeof schema.onValidationError === "function"
|
|
315
|
+
? { validationErrorHook: true as const }
|
|
316
|
+
: {}),
|
|
255
317
|
}
|
|
256
318
|
}
|
|
257
319
|
|
|
@@ -326,10 +388,12 @@ const reflectedSockets = (source: unknown): ReflectedRoute[] => {
|
|
|
326
388
|
*/
|
|
327
389
|
export function reflectRoutes(source: unknown): readonly ReflectedRoute[] {
|
|
328
390
|
const reflected: ReflectedRoute[] = []
|
|
391
|
+
// The app-wide `server({ onValidationError })` lives on the server, not on each route descriptor.
|
|
392
|
+
const appValidationHook = typeof recordOf(source)?.defaultOnValidationError === "function"
|
|
329
393
|
for (const candidate of routeCandidates(source)) {
|
|
330
394
|
const route = recordOf(candidate)
|
|
331
395
|
if (typeof route?.method !== "string" || typeof route.path !== "string") continue
|
|
332
|
-
const schema = reflectedRouteSchema(route.schema)
|
|
396
|
+
const schema = reflectedRouteSchema(route.schema, appValidationHook)
|
|
333
397
|
const tool = reflectedTool(route.tool)
|
|
334
398
|
const assurance = reflectedAssurance(route.assurance)
|
|
335
399
|
const capabilities = normalizeRouteCapabilities(
|
package/src/response-observer.ts
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
} from "./server/response-observer-runtime.ts"
|
|
6
6
|
import type { AnyServer } from "./server/server.ts"
|
|
7
7
|
|
|
8
|
+
export {
|
|
9
|
+
markRepeatedPrefix,
|
|
10
|
+
type RepeatedPrefix,
|
|
11
|
+
repeatedPrefixOf,
|
|
12
|
+
} from "./server/repeated-prefix.ts"
|
|
13
|
+
|
|
8
14
|
export type {
|
|
9
15
|
NodeResponseBodyHook,
|
|
10
16
|
ResponseBodyHook,
|