@nifrajs/core 2.14.1 → 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 +25 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- 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 +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.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 +17 -9
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -18
- 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/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- 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/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- 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/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.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/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.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 +2 -13
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +18 -159
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +58 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +76 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +42 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +6 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +158 -116
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +707 -844
- 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/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +12 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +50 -16
- 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 +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +472 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +28 -3
- package/src/assurance.ts +58 -1
- 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 +27 -0
- package/src/errors.ts +1 -0
- 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 +53 -96
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +77 -0
- package/src/server/body-lane.ts +203 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/plugin.ts +11 -5
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +13 -216
- package/src/server/respond.ts +71 -3
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +124 -1
- package/src/server/server-types.ts +6 -1
- package/src/server/server.ts +1446 -1485
- package/src/server/validation.ts +19 -0
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +67 -15
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +8 -2
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +526 -0
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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The all-lowercase header-name proof: one answer, computed once per request, read by everyone who
|
|
3
5
|
* would otherwise re-derive it.
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
* Declared with `Symbol.for` so the `@nifrajs/node` adapter can read it by key rather than importing
|
|
24
26
|
* across the package boundary - the same cross-package convention as the body and result marks.
|
|
25
27
|
*/
|
|
26
|
-
const LOWERCASE_HEADER_KEYS = Symbol.for(
|
|
28
|
+
const LOWERCASE_HEADER_KEYS = Symbol.for(NODE_BRIDGE_MARKER_KEYS.lowercaseHeaderKeys)
|
|
27
29
|
|
|
28
30
|
/** True when the name contains an ASCII `A`-`Z`, i.e. is not already the wire spelling. */
|
|
29
31
|
export function hasUpperAscii(name: string): boolean {
|
package/src/server/http.ts
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* the server can reuse them without pulling a lane's feature code into the base bundle.
|
|
2
|
+
* HTTP helpers shared by the kernel and the opt-in request lanes (idempotency, effect-ledger). Kept
|
|
3
|
+
* in a leaf module (runtime-core aside, itself a leaf) so a lane can reuse them without importing the
|
|
4
|
+
* server, and so the server can reuse them without pulling a lane's feature code into the base bundle.
|
|
5
5
|
*/
|
|
6
|
+
import { type ResponseResult, status as statusResult } from "./runtime-core.ts"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Is `value` a destination that stays on this origin?
|
|
10
|
+
*
|
|
11
|
+
* True only for an absolute path: one leading `/`, never `//` (protocol-relative → another origin),
|
|
12
|
+
* and free of any character a URL parser turns into an origin escape. A leading `/` alone is not
|
|
13
|
+
* enough: under a special scheme `\` parses as `/`, and tab/CR/LF are STRIPPED before parsing, so
|
|
14
|
+
* `/\evil.example` and `/<TAB>/evil.example` both resolve to the external host `evil.example` while
|
|
15
|
+
* passing a `//` test. Rejecting CR/LF here also keeps a validated destination out of the
|
|
16
|
+
* response-splitting sink it usually flows into.
|
|
17
|
+
*
|
|
18
|
+
* One implementation, because every caller is the same open-redirect gate: `redirect()` in
|
|
19
|
+
* `@nifrajs/web`, the `redirectTo` of the `@nifrajs/auth` and `@nifrajs/better-auth` guards. Three
|
|
20
|
+
* copies of a security predicate is three chances for one of them to be hardened alone.
|
|
21
|
+
*
|
|
22
|
+
* It answers about a PATH, not a URL: an absolute `https://this.host/x` is false even for the
|
|
23
|
+
* current origin, because the whole point is that the value never got to name a host.
|
|
24
|
+
*/
|
|
25
|
+
export function isSameOriginPath(value: string): boolean {
|
|
26
|
+
if (!value.startsWith("/") || value.startsWith("//")) return false
|
|
27
|
+
// Scanned by character code rather than matched by regex: no control characters have to be written
|
|
28
|
+
// into a pattern, and the check is a single pass over a short string.
|
|
29
|
+
for (let i = 1; i < value.length; i++) {
|
|
30
|
+
const code = value.charCodeAt(i)
|
|
31
|
+
if (code === 0x5c /* \ */ || code <= 0x1f || code === 0x7f) return false
|
|
32
|
+
}
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
6
35
|
|
|
7
36
|
/** A uniform JSON error envelope: `{ ok: false, error }` at the given status. */
|
|
8
37
|
export function jsonError(
|
|
@@ -16,6 +45,27 @@ export function jsonError(
|
|
|
16
45
|
)
|
|
17
46
|
}
|
|
18
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The same envelope as {@link jsonError}, as plain data rather than a built `Response`.
|
|
50
|
+
*
|
|
51
|
+
* This is what the framework's own renders - 404, 405, 422, the body caps, the timeouts, 500 - answer
|
|
52
|
+
* with wherever the value flows into a lane's response wrapper, so an error costs what a handler's
|
|
53
|
+
* plain return costs. Building the `Response` was measured as the dominant cost of answering early
|
|
54
|
+
* (see `PlainRender`), and on the Node lane an error `Response` is worse still: it is untagged, so
|
|
55
|
+
* the adapter cannot recognize its body and drains it through a Web stream, which is also why an
|
|
56
|
+
* error answered chunked while an ordinary return carries a `content-length`.
|
|
57
|
+
*
|
|
58
|
+
* `jsonError` stays for the callers that genuinely need a `Response` object (mount bridges, the
|
|
59
|
+
* lanes' own typed returns).
|
|
60
|
+
*/
|
|
61
|
+
export function plainError(
|
|
62
|
+
status: number,
|
|
63
|
+
error: string,
|
|
64
|
+
headers?: Record<string, string>,
|
|
65
|
+
): ResponseResult {
|
|
66
|
+
return statusResult(status, { ok: false, error }, headers !== undefined ? { headers } : undefined)
|
|
67
|
+
}
|
|
68
|
+
|
|
19
69
|
export interface UrlParts {
|
|
20
70
|
readonly pathname: string
|
|
21
71
|
readonly search: string
|
package/src/server/install.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - the seam stays off the server's public typed surface (symbol-keyed methods are not part of it).
|
|
7
7
|
* Both sides reference the SAME registered symbol, so no module has to import the other.
|
|
8
8
|
*/
|
|
9
|
+
import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
|
|
9
10
|
|
|
10
11
|
/** @internal Install the idempotency runtime on a server (called by the `idempotency()` plugin). */
|
|
11
12
|
export const INSTALL_IDEMPOTENCY: unique symbol = Symbol.for("@nifrajs/core/install-idempotency")
|
|
@@ -31,3 +32,16 @@ export const INSTALL_WS: unique symbol = Symbol.for("@nifrajs/core/install-ws")
|
|
|
31
32
|
export const INSTALL_RESPONSE_CONTRACT: unique symbol = Symbol.for(
|
|
32
33
|
"@nifrajs/core/install-response-contract",
|
|
33
34
|
)
|
|
35
|
+
|
|
36
|
+
/** @internal Install the response-observer adapters on a server. */
|
|
37
|
+
export const INSTALL_RESPONSE_OBSERVER: unique symbol = Symbol.for(
|
|
38
|
+
"@nifrajs/core/install-response-observer",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
/** @internal Resolve a native mount for an adapter that can keep the request on its own stream lane. */
|
|
42
|
+
export const RESOLVE_NODE_MOUNT: unique symbol = Symbol.for(
|
|
43
|
+
NODE_BRIDGE_MARKER_KEYS.resolveNodeMount,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/** @internal Marker on a mounted handler that knows how to serve a native Node request/response. */
|
|
47
|
+
export const NODE_NATIVE_MOUNT: unique symbol = Symbol.for("@nifrajs/core/node-native-mount")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Platform } from "./context.ts"
|
|
4
4
|
import { hasLowercaseHeaderKeysMark, hasUpperAscii } from "./header-case.ts"
|
|
5
5
|
import type { NodeServeOutcome } from "./node-outcome.ts"
|
|
6
|
+
import type { ResponseResult } from "./runtime-core.ts"
|
|
6
7
|
import type { CtxSet, MaybePromise } from "./server.ts"
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -309,7 +310,7 @@ export interface NodeOutcomeRuntime {
|
|
|
309
310
|
toOutcome(result: unknown, set: CtxSet): NodeServeOutcome
|
|
310
311
|
/** Materialize a buffered outcome for a Web response hook without losing its direct-write marker. */
|
|
311
312
|
toResponse(outcome: NodeServeOutcome): Response
|
|
312
|
-
fromResponse(response: Response): NodeServeOutcome
|
|
313
|
+
fromResponse(response: Response | ResponseResult): NodeServeOutcome
|
|
313
314
|
timeout(): NodeServeOutcome
|
|
314
315
|
/** The `Content-Type` this runtime's json render writes implicitly. The native hook walk
|
|
315
316
|
* materializes it into the hook-visible header record so a body hook's content-type checks see
|
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
rememberMutableHeaders,
|
|
12
12
|
taggedResponseBody,
|
|
13
13
|
} from "./respond.ts"
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
EMPTY_RESPONSE_CONTROLS,
|
|
16
|
+
type HandlerResult,
|
|
17
|
+
isResponseResult,
|
|
18
|
+
type PlainRender,
|
|
19
|
+
plainRenderHeaders,
|
|
20
|
+
type ResponseResult,
|
|
21
|
+
} from "./runtime-core.ts"
|
|
15
22
|
import type { CtxSet } from "./server.ts"
|
|
16
23
|
import {
|
|
17
24
|
mergeStaticHeaderRecord,
|
|
@@ -58,6 +65,8 @@ export type NodeServeOutcome =
|
|
|
58
65
|
*/
|
|
59
66
|
export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutcome {
|
|
60
67
|
if (isResponseResult(result)) {
|
|
68
|
+
const plain = result.plain
|
|
69
|
+
if (plain !== undefined) return plainNodeOutcome(plain, set)
|
|
61
70
|
const body = result.toNodeBody?.()
|
|
62
71
|
if (body !== undefined) {
|
|
63
72
|
return {
|
|
@@ -85,6 +94,24 @@ export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutc
|
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A `status(...)` (or any other {@link PlainRender} carrier) on the node lane: the same `kind: "json"`
|
|
99
|
+
* outcome a handler's plain return produces, so an early exit is written to the socket by the same
|
|
100
|
+
* direct writer, with a `content-length`, and never builds a `Response`.
|
|
101
|
+
*/
|
|
102
|
+
export function plainNodeOutcome(plain: PlainRender, set: CtxSet): NodeServeOutcome {
|
|
103
|
+
return {
|
|
104
|
+
kind: "json",
|
|
105
|
+
status: plain.status,
|
|
106
|
+
headers: plainRenderHeaders(plain, set),
|
|
107
|
+
cookies: set._cookies,
|
|
108
|
+
body:
|
|
109
|
+
plain.body === undefined || isBodylessStatus(plain.status)
|
|
110
|
+
? null
|
|
111
|
+
: JSON.stringify(plain.body),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
/**
|
|
89
116
|
* Materialize a buffered node outcome only when a Web `onResponse` hook needs to see a real
|
|
90
117
|
* `Response`. The marker lets an in-place hook (`response.headers.set(...); return response`) go back
|
|
@@ -162,8 +189,22 @@ export function withStaticNodeHeaders(
|
|
|
162
189
|
return { ...outcome, headers }
|
|
163
190
|
}
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The node lane's `wrapResponse`: what an early exit built OUTSIDE the handler's finalizer renders as
|
|
194
|
+
* - an `onRequest` hook's `Response`, a mount's, and every framework error render.
|
|
195
|
+
*
|
|
196
|
+
* A plain-data carrier ({@link plainError}, `status(...)`) takes the same `kind: "json"` lane a
|
|
197
|
+
* handler's plain return takes: no `Response` built, and none drained. `EMPTY_RESPONSE_CONTROLS`
|
|
198
|
+
* rather than the request's `c.set` on purpose - these renders happen where no context exists (before
|
|
199
|
+
* routing, or after it was abandoned), which is exactly why they are wrapped rather than finalized.
|
|
200
|
+
*/
|
|
201
|
+
export function nodeOutcomeFromResponse(result: Response | ResponseResult): NodeServeOutcome {
|
|
202
|
+
if (!(result instanceof Response)) {
|
|
203
|
+
const plain = result.plain
|
|
204
|
+
if (plain !== undefined) return plainNodeOutcome(plain, EMPTY_RESPONSE_CONTROLS)
|
|
205
|
+
return nodeOutcomeFromResponse(result.toResponse())
|
|
206
|
+
}
|
|
207
|
+
const response = normalizeBodylessResponse(result)
|
|
167
208
|
const body = nodeResponseBody(response)
|
|
168
209
|
return body === undefined
|
|
169
210
|
? { kind: "response", response }
|
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/query.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* and the byte-capped HTML-form reader. Split out of the server kernel so the file stays one domain.
|
|
5
5
|
*/
|
|
6
6
|
import { readBoundedBytes } from "./body.ts"
|
|
7
|
-
import {
|
|
7
|
+
import { plainError, urlPartsOf } from "./http.ts"
|
|
8
|
+
import type { ResponseResult } from "./runtime-core.ts"
|
|
8
9
|
import type { RequestSource } from "./server.ts"
|
|
9
10
|
|
|
10
11
|
// The query string ("?a=1", or "" when absent) - for lazily building `c.query` only when read.
|
|
@@ -123,16 +124,21 @@ const FORM_DECODER = new TextDecoder()
|
|
|
123
124
|
* Read an HTML-form body (urlencoded) into the plain object a body schema validates - same byte
|
|
124
125
|
* cap as the JSON path (a lying/absent Content-Length can't force an oversized buffer), same
|
|
125
126
|
* repeated-key -> `string[]` promotion as query parsing, same `__proto__` guard.
|
|
127
|
+
*
|
|
128
|
+
* A rejection is a `ResponseResult`, not a built `Response`, matching the JSON reader: the cap is a
|
|
129
|
+
* framework render, and answering one on the plain lane costs what a handler's plain return costs
|
|
130
|
+
* (see `plainError`). The distinction is observable on the Node adapter - a `Response` there is
|
|
131
|
+
* untagged, so its body drains through a Web stream and the answer goes out chunked.
|
|
126
132
|
*/
|
|
127
133
|
export async function readBoundedForm(
|
|
128
134
|
req: RequestSource,
|
|
129
135
|
maxBytes: number,
|
|
130
|
-
): Promise<Record<string, QueryValue> |
|
|
136
|
+
): Promise<Record<string, QueryValue> | ResponseResult> {
|
|
131
137
|
const read = await readBoundedBytes(req, maxBytes)
|
|
132
138
|
if (!read.ok) {
|
|
133
139
|
return read.status === 413
|
|
134
|
-
?
|
|
135
|
-
:
|
|
140
|
+
? plainError(413, "payload_too_large")
|
|
141
|
+
: plainError(400, "invalid_content_length")
|
|
136
142
|
}
|
|
137
143
|
const out: Record<string, QueryValue> = Object.create(null) as Record<string, QueryValue>
|
|
138
144
|
// URLSearchParams owns the format's quirks (`+` as space, percent-decoding, empty keys);
|
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
|
|