@nifrajs/core 3.3.0 → 4.0.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 +26 -0
- package/dist/assurance.d.ts +7 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +39 -3
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +20 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +47 -15
- package/dist/capabilities.js.map +1 -1
- package/dist/diff.d.ts +6 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +36 -2
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +16 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-effect.d.ts.map +1 -1
- package/dist/durable-effect.js +6 -3
- package/dist/durable-effect.js.map +1 -1
- package/dist/edge-kit.d.ts +2 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-lifecycle.d.ts.map +1 -1
- package/dist/effect-lifecycle.js +2 -1
- package/dist/effect-lifecycle.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +1 -1
- package/dist/effect-scope.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 +36 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +219 -13
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts +31 -0
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +48 -9
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +7 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +9 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.js +2 -0
- package/dist/internal/durable-saga-protocol.js.map +1 -1
- package/dist/internal/route-catalog.d.ts +6 -6
- package/dist/internal/route-catalog.d.ts.map +1 -1
- package/dist/internal/route-catalog.js +20 -18
- package/dist/internal/route-catalog.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +7 -6
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +36 -4
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +7 -3
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +1 -0
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +34 -29
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/route-program.d.ts +8 -2
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +222 -20
- package/dist/internal/route-program.js.map +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +8 -5
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +52 -46
- package/dist/manifest.js.map +1 -1
- package/dist/mount.d.ts +36 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +32 -0
- package/dist/mount.js.map +1 -1
- package/dist/reflection.d.ts +26 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +65 -1
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -1
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js.map +1 -1
- package/dist/router/overlap.d.ts +23 -0
- package/dist/router/overlap.d.ts.map +1 -0
- package/dist/router/overlap.js +224 -0
- package/dist/router/overlap.js.map +1 -0
- package/dist/router/pattern.d.ts +117 -2
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +283 -23
- package/dist/router/pattern.js.map +1 -1
- package/dist/router/router.d.ts +18 -4
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +51 -44
- package/dist/router/router.js.map +1 -1
- package/dist/server/auth.d.ts +53 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +9 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +12 -7
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +13 -11
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/body-lane.d.ts +20 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +26 -7
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts +68 -0
- package/dist/server/body-parser.d.ts.map +1 -0
- package/dist/server/body-parser.js +172 -0
- package/dist/server/body-parser.js.map +1 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +62 -7
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +8 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +8 -0
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +71 -6
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +15 -5
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +25 -4
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts +4 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +40 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.d.ts +2 -0
- package/dist/server/core-copies.d.ts.map +1 -0
- package/dist/server/core-copies.js +20 -0
- package/dist/server/core-copies.js.map +1 -0
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -0
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +36 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +130 -73
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +4 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +4 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/methods.d.ts +45 -0
- package/dist/server/methods.d.ts.map +1 -0
- package/dist/server/methods.js +37 -0
- package/dist/server/methods.js.map +1 -0
- package/dist/server/multipart.d.ts +63 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/multipart.js +374 -0
- package/dist/server/multipart.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts +20 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js +15 -6
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/not-found-answer.d.ts +40 -0
- package/dist/server/not-found-answer.d.ts.map +1 -0
- package/dist/server/not-found-answer.js +56 -0
- package/dist/server/not-found-answer.js.map +1 -0
- package/dist/server/not-found.d.ts +23 -0
- package/dist/server/not-found.d.ts.map +1 -0
- package/dist/server/not-found.js +81 -0
- package/dist/server/not-found.js.map +1 -0
- package/dist/server/plugin.d.ts +26 -1
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/proto-guard.d.ts +18 -5
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +114 -9
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts +7 -0
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +47 -0
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +13 -2
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +11 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +33 -19
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-lineage.d.ts +8 -0
- package/dist/server/request-lineage.d.ts.map +1 -0
- package/dist/server/request-lineage.js +14 -0
- package/dist/server/request-lineage.js.map +1 -0
- package/dist/server/request-target.d.ts +31 -0
- package/dist/server/request-target.d.ts.map +1 -0
- package/dist/server/request-target.js +44 -0
- package/dist/server/request-target.js.map +1 -0
- package/dist/server/respond.d.ts +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +16 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +19 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +59 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +9 -5
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -6
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +55 -3
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +194 -19
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1342 -274
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +10 -7
- package/dist/server/sse.js.map +1 -1
- package/dist/server/static-headers.d.ts +3 -0
- package/dist/server/static-headers.d.ts.map +1 -1
- package/dist/server/static-headers.js +19 -1
- package/dist/server/static-headers.js.map +1 -1
- package/dist/server/transport-lane.d.ts +8 -0
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +9 -8
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/validation.d.ts.map +1 -1
- package/dist/server/validation.js +5 -1
- package/dist/server/validation.js.map +1 -1
- package/dist/server/websocket.d.ts +10 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +7 -4
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +2 -0
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +6 -3
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +2 -1
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +8 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.js +5 -0
- package/dist/single-copy-register.js.map +1 -1
- package/dist/single-copy.d.ts +46 -3
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +125 -12
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts +23 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +37 -3
- package/dist/tool-contract.js.map +1 -1
- 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 +27 -2
- package/src/assurance.ts +52 -2
- package/src/capabilities.ts +81 -22
- package/src/diff.ts +54 -7
- package/src/durable-adapters.ts +20 -3
- package/src/durable-effect.ts +10 -3
- package/src/edge-kit.ts +7 -0
- package/src/effect-lifecycle.ts +5 -1
- package/src/effect-scope.ts +2 -1
- package/src/errors.ts +3 -0
- package/src/evidence.ts +302 -12
- package/src/idempotency.ts +80 -9
- package/src/index.ts +41 -3
- package/src/internal/code-unit-order.ts +8 -0
- package/src/internal/durable-saga-protocol.ts +2 -0
- package/src/internal/route-catalog.ts +19 -18
- package/src/internal/route-compiler.ts +62 -13
- package/src/internal/route-execution.ts +13 -3
- package/src/internal/route-lanes.ts +37 -28
- package/src/internal/route-program.ts +434 -20
- package/src/ledger.ts +11 -5
- package/src/manifest.ts +52 -46
- package/src/mount.ts +62 -0
- package/src/reflection.ts +84 -1
- package/src/response-observer.ts +1 -0
- package/src/router/overlap.ts +258 -0
- package/src/router/pattern.ts +349 -25
- package/src/router/router.ts +76 -51
- package/src/server/auth.ts +75 -0
- package/src/server/bare-error-lane.ts +12 -11
- package/src/server/body-lane.ts +46 -6
- package/src/server/body-parser.ts +241 -0
- package/src/server/body.ts +82 -9
- package/src/server/client-ip.ts +9 -0
- package/src/server/context.ts +219 -12
- package/src/server/contract.ts +54 -9
- package/src/server/cookies.ts +34 -2
- package/src/server/core-copies.ts +21 -0
- package/src/server/edge.ts +2 -0
- package/src/server/http.ts +34 -0
- package/src/server/idempotency-lane.ts +147 -72
- package/src/server/install.ts +6 -0
- package/src/server/methods.ts +143 -0
- package/src/server/multipart.ts +424 -0
- package/src/server/node-outcome-hook.ts +40 -7
- package/src/server/not-found-answer.ts +109 -0
- package/src/server/not-found.ts +121 -0
- package/src/server/plugin.ts +45 -1
- package/src/server/proto-guard.ts +128 -9
- package/src/server/query.ts +49 -0
- package/src/server/registry.ts +17 -2
- package/src/server/request-context.ts +44 -16
- package/src/server/request-lineage.ts +15 -0
- package/src/server/request-target.ts +46 -0
- package/src/server/respond.ts +14 -3
- package/src/server/response-contract-lane.ts +97 -16
- package/src/server/response-observer-runtime.ts +37 -8
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +61 -2
- package/src/server/server.ts +1798 -355
- package/src/server/sse.ts +10 -8
- package/src/server/static-headers.ts +22 -1
- package/src/server/transport-lane.ts +17 -9
- package/src/server/validation.ts +7 -1
- package/src/server/websocket.ts +23 -9
- package/src/server/ws-bun.ts +8 -3
- package/src/server/ws-hook.ts +2 -1
- package/src/server.ts +39 -2
- package/src/single-copy-register.ts +5 -0
- package/src/single-copy.ts +160 -12
- package/src/tool-contract.ts +66 -3
- package/src/transport-codec.ts +2 -1
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
} from "../schema/standard.ts"
|
|
49
49
|
import { INSTALL_RESPONSE_CONTRACT } from "./install.ts"
|
|
50
50
|
import type { IdentityPlugin } from "./plugin.ts"
|
|
51
|
-
import { isResponseResult } from "./runtime-core.ts"
|
|
51
|
+
import { isResponseResult, status } from "./runtime-core.ts"
|
|
52
52
|
import type { AnyServer } from "./server.ts"
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -62,6 +62,12 @@ import type { AnyServer } from "./server.ts"
|
|
|
62
62
|
*/
|
|
63
63
|
export type ResponseContractMode = "warn" | "enforce"
|
|
64
64
|
|
|
65
|
+
/** Success and status-specific error schemas captured by one route at registration time. */
|
|
66
|
+
export interface ResponseContractDefinition {
|
|
67
|
+
readonly response?: StandardSchemaV1
|
|
68
|
+
readonly errors?: Readonly<Record<number, StandardSchemaV1>>
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
/** The outcome of checking one handler result against its declared response schema. */
|
|
66
72
|
export type ResponseContractOutcome =
|
|
67
73
|
/** Nothing to report. `value` is what should be serialized (the original result, or the cleaned one). */
|
|
@@ -91,38 +97,104 @@ export function checkResponseContract(
|
|
|
91
97
|
result: unknown,
|
|
92
98
|
mode: "warn" | "enforce",
|
|
93
99
|
): ResponseContractOutcome | Promise<ResponseContractOutcome> {
|
|
100
|
+
return checkRouteResponseContract({ response: schema }, result, mode)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The schema a reply with this status is held to: `response` for 2xx, else `errors[status]`. */
|
|
104
|
+
const schemaFor = (definition: ResponseContractDefinition, code: number) =>
|
|
105
|
+
code >= 200 && code < 300 ? definition.response : definition.errors?.[code]
|
|
106
|
+
|
|
107
|
+
/** A `c.json(...)` reply rebuilt around its validated value, keeping its status and headers. */
|
|
108
|
+
function reserialize(reply: Response, value: unknown): Response {
|
|
109
|
+
const headers = new Headers(reply.headers)
|
|
110
|
+
headers.delete("content-length")
|
|
111
|
+
return new Response(JSON.stringify(value), {
|
|
112
|
+
status: reply.status,
|
|
113
|
+
statusText: reply.statusText,
|
|
114
|
+
headers,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check a route's success or status-specific error payload. Plain `status()` results retain their
|
|
120
|
+
* status while enforcement replaces only the body with the validator's output. A `c.json(...)`
|
|
121
|
+
* reply is held to the contract through the value it was built from (`jsonReply`). Any other raw
|
|
122
|
+
* `Response` remains an explicit transport escape hatch because inspecting it would consume
|
|
123
|
+
* streams or alter redirects; assurance/static checks must govern that path separately.
|
|
124
|
+
*/
|
|
125
|
+
export function checkRouteResponseContract(
|
|
126
|
+
definition: ResponseContractDefinition,
|
|
127
|
+
result: unknown,
|
|
128
|
+
mode: "warn" | "enforce",
|
|
129
|
+
jsonReply?: { readonly body: unknown },
|
|
130
|
+
): ResponseContractOutcome | Promise<ResponseContractOutcome> {
|
|
131
|
+
const plain = isResponseResult(result) ? result.plain : undefined
|
|
132
|
+
if (plain !== undefined) {
|
|
133
|
+
const schema = schemaFor(definition, plain.status)
|
|
134
|
+
if (schema === undefined) return { kind: "ok", value: result }
|
|
135
|
+
return validateContract(schema, plain.body, result, mode, (value) =>
|
|
136
|
+
status(
|
|
137
|
+
plain.status,
|
|
138
|
+
value,
|
|
139
|
+
plain.headers === undefined ? undefined : { headers: plain.headers },
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
if (jsonReply !== undefined && result instanceof Response) {
|
|
144
|
+
const schema = schemaFor(definition, result.status)
|
|
145
|
+
if (schema === undefined) return { kind: "ok", value: result }
|
|
146
|
+
return validateContract(schema, jsonReply.body, result, mode, (value) =>
|
|
147
|
+
reserialize(result, value),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
94
151
|
// A handler may return a raw Response as deliberate control flow (a redirect, a stream). There is no
|
|
95
152
|
// JSON payload to hold to the contract, and re-serializing one would corrupt it. A `status(...)` is
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
if (result instanceof Response || result === undefined
|
|
99
|
-
|
|
100
|
-
|
|
153
|
+
// intentionally handled above because its plain body and status remain available without consuming
|
|
154
|
+
// a stream. An absent body has nothing to validate.
|
|
155
|
+
if (result instanceof Response || result === undefined) return { kind: "ok", value: result }
|
|
156
|
+
const schema = definition.response
|
|
157
|
+
if (schema === undefined) return { kind: "ok", value: result }
|
|
158
|
+
return validateContract(schema, result, result, mode, (value) => value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Validate `payload`; `served` is what goes out unchanged when nothing is enforced - the whole
|
|
162
|
+
* `status(...)` result or reply, never just its body. */
|
|
163
|
+
function validateContract(
|
|
164
|
+
schema: StandardSchemaV1,
|
|
165
|
+
payload: unknown,
|
|
166
|
+
served: unknown,
|
|
167
|
+
mode: "warn" | "enforce",
|
|
168
|
+
replace: (value: unknown) => unknown,
|
|
169
|
+
): ResponseContractOutcome | Promise<ResponseContractOutcome> {
|
|
170
|
+
const settled = schema["~standard"].validate(payload)
|
|
101
171
|
return settled instanceof Promise
|
|
102
|
-
? settled.then((r) => interpret(r,
|
|
103
|
-
: interpret(settled,
|
|
172
|
+
? settled.then((r) => interpret(r, payload, served, mode, replace))
|
|
173
|
+
: interpret(settled, payload, served, mode, replace)
|
|
104
174
|
}
|
|
105
175
|
|
|
106
176
|
function interpret(
|
|
107
177
|
settled: StandardResult<unknown>,
|
|
108
|
-
|
|
178
|
+
payload: unknown,
|
|
179
|
+
served: unknown,
|
|
109
180
|
mode: "warn" | "enforce",
|
|
181
|
+
replace: (value: unknown) => unknown,
|
|
110
182
|
): ResponseContractOutcome {
|
|
111
183
|
if (settled.issues !== undefined) {
|
|
112
184
|
const message = `response does not satisfy its declared contract: ${formatStandardIssues(settled.issues)}`
|
|
113
185
|
// `warn` never changes what is served - it reports and gets out of the way, so switching it on can
|
|
114
186
|
// never be the thing that broke production.
|
|
115
187
|
return mode === "warn"
|
|
116
|
-
? { kind: "warn", value:
|
|
188
|
+
? { kind: "warn", value: served, message }
|
|
117
189
|
: { kind: "violation", message }
|
|
118
190
|
}
|
|
119
|
-
const dropped = droppedKeys(
|
|
120
|
-
if (mode === "enforce") return { kind: "ok", value: settled.value }
|
|
191
|
+
const dropped = droppedKeys(payload, settled.value)
|
|
192
|
+
if (mode === "enforce") return { kind: "ok", value: replace(settled.value) }
|
|
121
193
|
return dropped.length === 0
|
|
122
|
-
? { kind: "ok", value:
|
|
194
|
+
? { kind: "ok", value: served }
|
|
123
195
|
: {
|
|
124
196
|
kind: "warn",
|
|
125
|
-
value:
|
|
197
|
+
value: served,
|
|
126
198
|
message: `response carries fields its contract does not declare: ${dropped.join(", ")}. They are being sent; \`responseContract: "enforce"\` would strip them.`,
|
|
127
199
|
}
|
|
128
200
|
}
|
|
@@ -134,8 +206,9 @@ function interpret(
|
|
|
134
206
|
export interface ResponseContractRuntime {
|
|
135
207
|
readonly mode: ResponseContractMode
|
|
136
208
|
check(
|
|
137
|
-
|
|
209
|
+
definition: ResponseContractDefinition,
|
|
138
210
|
result: unknown,
|
|
211
|
+
context?: { readonly jsonReply?: Response | undefined; readonly jsonBody?: unknown },
|
|
139
212
|
): ResponseContractOutcome | Promise<ResponseContractOutcome>
|
|
140
213
|
}
|
|
141
214
|
|
|
@@ -161,7 +234,15 @@ interface ResponseContractInstallable {
|
|
|
161
234
|
export function responseContract(mode: ResponseContractMode = "warn"): IdentityPlugin {
|
|
162
235
|
const runtime: ResponseContractRuntime = {
|
|
163
236
|
mode,
|
|
164
|
-
check: (
|
|
237
|
+
check: (definition, result, context) =>
|
|
238
|
+
checkRouteResponseContract(
|
|
239
|
+
definition,
|
|
240
|
+
result,
|
|
241
|
+
mode,
|
|
242
|
+
result instanceof Response && result === context?.jsonReply
|
|
243
|
+
? { body: context.jsonBody }
|
|
244
|
+
: undefined,
|
|
245
|
+
),
|
|
165
246
|
}
|
|
166
247
|
const apply = <S extends AnyServer>(app: S): S => {
|
|
167
248
|
;(app as unknown as ResponseContractInstallable)[INSTALL_RESPONSE_CONTRACT](runtime)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
NodeRequestContext,
|
|
3
|
+
NodeResponseBodyHook,
|
|
3
4
|
NodeResponseHook,
|
|
4
5
|
ResponseBodyHook,
|
|
5
6
|
ResponseHeadersHook,
|
|
@@ -12,18 +13,30 @@ import { applyBodyReplacement, withReplacedBody } from "./response-hooks.ts"
|
|
|
12
13
|
type MaybePromise<T> = T | Promise<T>
|
|
13
14
|
type WebResponseHook = (response: Response, req: Request) => MaybePromise<Response>
|
|
14
15
|
|
|
15
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
NodeResponseBodyHook,
|
|
18
|
+
ResponseBodyHook,
|
|
19
|
+
ResponseHeadersHook,
|
|
20
|
+
} from "./node-outcome-hook.ts"
|
|
16
21
|
|
|
17
22
|
export interface ResponseObserverHost {
|
|
18
23
|
assertConfigurable(operation: string): void
|
|
19
|
-
addResponseHook(
|
|
24
|
+
addResponseHook(
|
|
25
|
+
web: WebResponseHook,
|
|
26
|
+
node: NodeResponseHook | undefined,
|
|
27
|
+
headerOnly?: boolean,
|
|
28
|
+
): void
|
|
20
29
|
enableResponseBodyTagging(): object
|
|
21
30
|
responseBodyOwners(): ReadonlySet<object>
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export interface ResponseObserverMethods {
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Register a portable header hook. `node` is an optional equivalent for the Node-direct record;
|
|
36
|
+
* it must preserve the portable hook's observable header semantics.
|
|
37
|
+
*/
|
|
38
|
+
onResponseHeaders(fn: ResponseHeadersHook, node?: NodeResponseHook): this
|
|
39
|
+
onResponseBody(fn: ResponseBodyHook, node?: NodeResponseBodyHook): this
|
|
27
40
|
onResponseRaw(fn: (response: Response, req: Request) => MaybePromise<Response>): this
|
|
28
41
|
}
|
|
29
42
|
|
|
@@ -81,7 +94,11 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
|
|
|
81
94
|
return {
|
|
82
95
|
install(host) {
|
|
83
96
|
return {
|
|
84
|
-
onResponseHeaders(
|
|
97
|
+
onResponseHeaders(
|
|
98
|
+
this: ResponseObserverMethods,
|
|
99
|
+
fn: ResponseHeadersHook,
|
|
100
|
+
node?: NodeResponseHook,
|
|
101
|
+
): ResponseObserverMethods {
|
|
85
102
|
host.assertConfigurable("onResponseHeaders()")
|
|
86
103
|
host.addResponseHook(
|
|
87
104
|
(response, req) => {
|
|
@@ -89,11 +106,16 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
|
|
|
89
106
|
const out = fn(view.headers, view.request, response.status)
|
|
90
107
|
return out instanceof Promise ? out.then(() => view.response) : view.response
|
|
91
108
|
},
|
|
92
|
-
(response, req) => fn(recordHeadersView(response), req, response.status),
|
|
109
|
+
node ?? ((response, req) => fn(recordHeadersView(response), req, response.status)),
|
|
110
|
+
true,
|
|
93
111
|
)
|
|
94
112
|
return this
|
|
95
113
|
},
|
|
96
|
-
onResponseBody(
|
|
114
|
+
onResponseBody(
|
|
115
|
+
this: ResponseObserverMethods,
|
|
116
|
+
fn: ResponseBodyHook,
|
|
117
|
+
node?: NodeResponseBodyHook,
|
|
118
|
+
): ResponseObserverMethods {
|
|
97
119
|
host.assertConfigurable("onResponseBody()")
|
|
98
120
|
host.enableResponseBodyTagging()
|
|
99
121
|
host.addResponseHook(
|
|
@@ -108,12 +130,18 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
|
|
|
108
130
|
},
|
|
109
131
|
(response, req) => {
|
|
110
132
|
if (response.body === null) return undefined
|
|
111
|
-
|
|
133
|
+
// An author-supplied twin sees the same bytes and returns through the same
|
|
134
|
+
// replacement channel; only the header-view allocation is skipped.
|
|
135
|
+
const out =
|
|
136
|
+
node === undefined
|
|
137
|
+
? fn(response.body, recordHeadersView(response), req, response.status)
|
|
138
|
+
: node(response.body, response, req, response.status)
|
|
112
139
|
if (out instanceof Promise)
|
|
113
140
|
return out.then((replaced) => applyBodyReplacement(response, replaced))
|
|
114
141
|
applyBodyReplacement(response, out)
|
|
115
142
|
return undefined
|
|
116
143
|
},
|
|
144
|
+
false,
|
|
117
145
|
)
|
|
118
146
|
return this
|
|
119
147
|
},
|
|
@@ -126,6 +154,7 @@ export function createResponseObserverRuntime(): ResponseObserverRuntime {
|
|
|
126
154
|
? fn(response, req)
|
|
127
155
|
: response,
|
|
128
156
|
() => undefined,
|
|
157
|
+
false,
|
|
129
158
|
)
|
|
130
159
|
return this
|
|
131
160
|
},
|
|
@@ -27,6 +27,8 @@ export const PRE_DECODED_BODY = Symbol("nifra.body.preDecoded")
|
|
|
27
27
|
/** The stash shape under {@link PRE_DECODED_BODY}. */
|
|
28
28
|
export interface PreDecodedBody {
|
|
29
29
|
readonly value: unknown
|
|
30
|
+
/** The encoded size, so the route's own body limit still applies to a body decoded before routing. */
|
|
31
|
+
readonly byteLength: number
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -5,17 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { CapabilityUseEvent } from "../internal/capability-runtime.ts"
|
|
7
7
|
import type { AssuranceEvidence } from "../internal/route-assurance.ts"
|
|
8
|
-
import type {
|
|
8
|
+
import type { RouteMethod } from "../router/router.ts"
|
|
9
9
|
import type { ClientIpTrust } from "./client-ip.ts"
|
|
10
10
|
import type { Context, Platform, RouteSchema } from "./context.ts"
|
|
11
11
|
import type { Logger } from "./logger.ts"
|
|
12
12
|
import type {
|
|
13
13
|
NodeRequestHook,
|
|
14
|
+
NodeResponseBodyHook,
|
|
14
15
|
NodeResponseHook,
|
|
15
16
|
ResponseBodyHook,
|
|
16
17
|
ResponseHeadersHook,
|
|
17
18
|
} from "./node-outcome-hook.ts"
|
|
18
19
|
import type { MaybePromise, OnRequestResult } from "./server.ts"
|
|
20
|
+
import type { WebSocketUpgradeOutcome } from "./websocket.ts"
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* The outcome of a capacity-admission decision. `admitted` requests carry a `release` the server calls
|
|
@@ -85,6 +87,8 @@ export interface ServerOptions {
|
|
|
85
87
|
readonly wsMaxPayloadBytes?: number
|
|
86
88
|
/** Per-request timeout (ms): a slower request gets a 503 and `ctx.signal` aborts. 0 disables (default). */
|
|
87
89
|
readonly requestTimeoutMs?: number
|
|
90
|
+
/** WebSocket upgrade admission/guard timeout (ms). Default 10_000; set to 0 only for a deliberate unbounded guard. */
|
|
91
|
+
readonly wsUpgradeTimeoutMs?: number
|
|
88
92
|
/**
|
|
89
93
|
* How to derive `c.clientIp` behind a reverse proxy or CDN. Omit (default) to trust only the raw
|
|
90
94
|
* socket peer and never believe a forwarded header. Set `{ trustedHops: n }` when the app sits
|
|
@@ -167,10 +171,46 @@ export type FetchHandler<Env = unknown> = (
|
|
|
167
171
|
platform?: Platform<Env>,
|
|
168
172
|
) => MaybePromise<Response>
|
|
169
173
|
|
|
174
|
+
/** A fetch app that may also participate in adapter-owned WebSocket upgrades when mounted. */
|
|
175
|
+
export interface MountableApp<Env = unknown> {
|
|
176
|
+
readonly fetch: FetchHandler<Env>
|
|
177
|
+
readonly resolveWebSocketUpgrade?: (
|
|
178
|
+
request: Request,
|
|
179
|
+
platform?: Platform<Env>,
|
|
180
|
+
) => MaybePromise<WebSocketUpgradeOutcome>
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Options for a first-class composed mount. */
|
|
184
|
+
export interface MountOptions<Env = unknown> {
|
|
185
|
+
/** Absolute pathname prefix, optionally ending in `/*`. */
|
|
186
|
+
readonly path: string
|
|
187
|
+
/** The child app. Its HTTP fetch and optional WebSocket resolver share the mount. */
|
|
188
|
+
readonly app: MountableApp<Env>
|
|
189
|
+
/** Rewrite the child request to remove `path` before dispatch. Default: false. */
|
|
190
|
+
readonly stripPrefix?: boolean
|
|
191
|
+
/** Higher values run first; ties use the most specific path, then declaration order. */
|
|
192
|
+
readonly priority?: number
|
|
193
|
+
/** For safe replayable methods only, try the next matching mount when this one returns 404. */
|
|
194
|
+
readonly fallbackOn?: 404
|
|
195
|
+
/**
|
|
196
|
+
* Why this child is not analyzed, for a handler whose effects nifra cannot follow (a closure from
|
|
197
|
+
* another package). Capability assurance lists a mount with a reason as a known gap; a mount
|
|
198
|
+
* without one fails it, since its routes are invisible. A nifra `server()` belongs in `merge()`,
|
|
199
|
+
* where its routes are analyzed.
|
|
200
|
+
*/
|
|
201
|
+
readonly opaque?: string
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
/** Options for a legacy fetch-handler mount. */
|
|
171
205
|
export interface MountFetchOptions {
|
|
172
206
|
/** Remove the mount prefix before invoking the legacy handler. Default: false. */
|
|
173
207
|
readonly stripPrefix?: boolean
|
|
208
|
+
/** Higher values run first; ties use the most specific path, then declaration order. */
|
|
209
|
+
readonly priority?: number
|
|
210
|
+
/** For safe replayable methods only, try the next matching legacy mount when it returns 404. */
|
|
211
|
+
readonly fallbackOn?: 404
|
|
212
|
+
/** Why this handler is not analyzed - see {@link MountOptions.opaque}. */
|
|
213
|
+
readonly opaque?: string
|
|
174
214
|
}
|
|
175
215
|
|
|
176
216
|
/** A callback awaited after the Bun server has drained and stopped. */
|
|
@@ -200,9 +240,11 @@ export interface ToolAnnotations {
|
|
|
200
240
|
* (e.g. `toOpenAPI`) enumerate routes after registration.
|
|
201
241
|
*/
|
|
202
242
|
export interface RouteDescriptor {
|
|
203
|
-
readonly method:
|
|
243
|
+
readonly method: RouteMethod
|
|
204
244
|
readonly path: string
|
|
205
245
|
readonly schema: RouteSchema | undefined
|
|
246
|
+
/** Runtime response-contract mode captured when this route was registered. */
|
|
247
|
+
readonly responseContract?: "warn" | "enforce"
|
|
206
248
|
/** Effective enforcement evidence, populated only during reflection/introspection. */
|
|
207
249
|
readonly assurance?: readonly AssuranceEvidence[]
|
|
208
250
|
/** Normalized declared effect tokens, populated only when the route declares any. */
|
|
@@ -246,6 +288,19 @@ export interface McpPromptDescriptor {
|
|
|
246
288
|
readonly handler: (args: Record<string, string>) => MaybePromise<readonly PromptMessage[]>
|
|
247
289
|
}
|
|
248
290
|
|
|
291
|
+
/**
|
|
292
|
+
* TLS for `listen()`: serve HTTPS from Bun itself, with no proxy in front. `cert` and `key` are PEM,
|
|
293
|
+
* as text or as the file's bytes (`readFileSync("cert.pem")`); `passphrase` unlocks an encrypted key.
|
|
294
|
+
*/
|
|
295
|
+
export interface ListenTlsOptions {
|
|
296
|
+
/** The certificate chain, PEM. */
|
|
297
|
+
readonly cert: string | Uint8Array
|
|
298
|
+
/** The private key, PEM. */
|
|
299
|
+
readonly key: string | Uint8Array
|
|
300
|
+
/** The passphrase of an encrypted `key`. */
|
|
301
|
+
readonly passphrase?: string
|
|
302
|
+
}
|
|
303
|
+
|
|
249
304
|
/**
|
|
250
305
|
* The handle `listen()` returns - the slice of Bun's server nifra holds and exposes.
|
|
251
306
|
* Declared explicitly (rather than `ReturnType<typeof Bun.serve>`) so the public type
|
|
@@ -301,6 +356,8 @@ export interface Middleware {
|
|
|
301
356
|
* an optional `onNodeResponse` twin only when the hook must see a real `Response`.
|
|
302
357
|
*/
|
|
303
358
|
readonly onResponseHeaders?: ResponseHeadersHook
|
|
359
|
+
/** Allocation-light Node-direct twin for a portable header-only hook; keep its wire semantics identical. */
|
|
360
|
+
readonly onNodeResponseHeaders?: NodeResponseHook
|
|
304
361
|
/**
|
|
305
362
|
* PORTABLE post-serialization body hook - the payload tier. Receives the final
|
|
306
363
|
* framework-serialized bytes (already resident on every runtime; no stream is drained) plus the
|
|
@@ -308,6 +365,8 @@ export interface Middleware {
|
|
|
308
365
|
* (proxies, SSE, streamed SSR) are skipped by contract - transforming those needs `onResponse`.
|
|
309
366
|
*/
|
|
310
367
|
readonly onResponseBody?: ResponseBodyHook
|
|
368
|
+
/** Allocation-light Node-direct twin for a portable body hook; keep its wire semantics identical. */
|
|
369
|
+
readonly onNodeResponseBody?: NodeResponseBodyHook
|
|
311
370
|
/**
|
|
312
371
|
* Response transform for untagged/raw responses (streams, proxied fetches, and framework errors).
|
|
313
372
|
* Framework-serialized payloads use `onResponseBody` and remain on the Node direct lane.
|