@nifrajs/core 1.13.0 → 2.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.
Files changed (157) hide show
  1. package/README.md +10 -1
  2. package/dist/assurance.d.ts +11 -2
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +20 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/effect-ledger.d.ts +9 -0
  7. package/dist/effect-ledger.d.ts.map +1 -0
  8. package/dist/effect-ledger.js +8 -0
  9. package/dist/effect-ledger.js.map +1 -0
  10. package/dist/idempotency-plugin.d.ts +8 -0
  11. package/dist/idempotency-plugin.d.ts.map +1 -0
  12. package/dist/idempotency-plugin.js +8 -0
  13. package/dist/idempotency-plugin.js.map +1 -0
  14. package/dist/index.d.ts +6 -35
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -31
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp.d.ts +7 -0
  19. package/dist/mcp.d.ts.map +1 -0
  20. package/dist/mcp.js +56 -0
  21. package/dist/mcp.js.map +1 -0
  22. package/dist/node-direct.d.ts +5 -0
  23. package/dist/node-direct.d.ts.map +1 -0
  24. package/dist/node-direct.js +27 -0
  25. package/dist/node-direct.js.map +1 -0
  26. package/dist/reflection.d.ts +5 -0
  27. package/dist/reflection.d.ts.map +1 -1
  28. package/dist/reflection.js +2 -0
  29. package/dist/reflection.js.map +1 -1
  30. package/dist/server/client-ip.d.ts +35 -0
  31. package/dist/server/client-ip.d.ts.map +1 -0
  32. package/dist/server/client-ip.js +42 -0
  33. package/dist/server/client-ip.js.map +1 -0
  34. package/dist/server/context.d.ts +47 -2
  35. package/dist/server/context.d.ts.map +1 -1
  36. package/dist/server/contract.d.ts +14 -4
  37. package/dist/server/contract.d.ts.map +1 -1
  38. package/dist/server/contract.js +8 -2
  39. package/dist/server/contract.js.map +1 -1
  40. package/dist/server/http.d.ts +14 -0
  41. package/dist/server/http.d.ts.map +1 -0
  42. package/dist/server/http.js +45 -0
  43. package/dist/server/http.js.map +1 -0
  44. package/dist/server/idempotency-lane.d.ts +44 -0
  45. package/dist/server/idempotency-lane.d.ts.map +1 -0
  46. package/dist/server/idempotency-lane.js +181 -0
  47. package/dist/server/idempotency-lane.js.map +1 -0
  48. package/dist/server/install.d.ts +21 -0
  49. package/dist/server/install.d.ts.map +1 -0
  50. package/dist/server/install.js +21 -0
  51. package/dist/server/install.js.map +1 -0
  52. package/dist/server/ledger-lane.d.ts +53 -0
  53. package/dist/server/ledger-lane.d.ts.map +1 -0
  54. package/dist/server/ledger-lane.js +82 -0
  55. package/dist/server/ledger-lane.js.map +1 -0
  56. package/dist/server/mcp-hook.d.ts +28 -0
  57. package/dist/server/mcp-hook.d.ts.map +1 -0
  58. package/dist/server/mcp-hook.js +2 -0
  59. package/dist/server/mcp-hook.js.map +1 -0
  60. package/dist/server/node-outcome-hook.d.ts +9 -0
  61. package/dist/server/node-outcome-hook.d.ts.map +1 -0
  62. package/dist/server/node-outcome-hook.js +2 -0
  63. package/dist/server/node-outcome-hook.js.map +1 -0
  64. package/dist/server/node-outcome.d.ts +37 -0
  65. package/dist/server/node-outcome.d.ts.map +1 -0
  66. package/dist/server/node-outcome.js +75 -0
  67. package/dist/server/node-outcome.js.map +1 -0
  68. package/dist/server/plugin.d.ts +78 -0
  69. package/dist/server/plugin.d.ts.map +1 -0
  70. package/dist/server/plugin.js +57 -0
  71. package/dist/server/plugin.js.map +1 -0
  72. package/dist/server/query.d.ts +18 -0
  73. package/dist/server/query.d.ts.map +1 -0
  74. package/dist/server/query.js +102 -0
  75. package/dist/server/query.js.map +1 -0
  76. package/dist/server/registry.d.ts +31 -6
  77. package/dist/server/registry.d.ts.map +1 -1
  78. package/dist/server/request-context.d.ts +51 -0
  79. package/dist/server/request-context.d.ts.map +1 -0
  80. package/dist/server/request-context.js +184 -0
  81. package/dist/server/request-context.js.map +1 -0
  82. package/dist/server/respond.d.ts +17 -0
  83. package/dist/server/respond.d.ts.map +1 -0
  84. package/dist/server/respond.js +81 -0
  85. package/dist/server/respond.js.map +1 -0
  86. package/dist/server/runtime-core.d.ts +36 -0
  87. package/dist/server/runtime-core.d.ts.map +1 -0
  88. package/dist/server/runtime-core.js +51 -0
  89. package/dist/server/runtime-core.js.map +1 -0
  90. package/dist/server/server-types.d.ts +203 -0
  91. package/dist/server/server-types.d.ts.map +1 -0
  92. package/dist/server/server-types.js +2 -0
  93. package/dist/server/server-types.js.map +1 -0
  94. package/dist/server/server.d.ts +107 -345
  95. package/dist/server/server.d.ts.map +1 -1
  96. package/dist/server/server.js +214 -831
  97. package/dist/server/server.js.map +1 -1
  98. package/dist/server/sse-hook.d.ts +10 -0
  99. package/dist/server/sse-hook.d.ts.map +1 -0
  100. package/dist/server/sse-hook.js +2 -0
  101. package/dist/server/sse-hook.js.map +1 -0
  102. package/dist/server/sse.d.ts +7 -23
  103. package/dist/server/sse.d.ts.map +1 -1
  104. package/dist/server/sse.js +18 -0
  105. package/dist/server/sse.js.map +1 -1
  106. package/dist/server/websocket.d.ts +17 -1
  107. package/dist/server/websocket.d.ts.map +1 -1
  108. package/dist/server/websocket.js.map +1 -1
  109. package/dist/server/ws-hook.d.ts +8 -11
  110. package/dist/server/ws-hook.d.ts.map +1 -1
  111. package/dist/server/ws-hook.js +7 -14
  112. package/dist/server/ws-hook.js.map +1 -1
  113. package/dist/server.d.ts +21 -0
  114. package/dist/server.d.ts.map +1 -0
  115. package/dist/server.js +16 -0
  116. package/dist/server.js.map +1 -0
  117. package/dist/ws.d.ts +16 -7
  118. package/dist/ws.d.ts.map +1 -1
  119. package/dist/ws.js +25 -10
  120. package/dist/ws.js.map +1 -1
  121. package/package.json +77 -6
  122. package/src/assurance.ts +36 -1
  123. package/src/effect-ledger.ts +9 -0
  124. package/src/idempotency-plugin.ts +7 -0
  125. package/src/index.ts +6 -133
  126. package/src/mcp.ts +65 -0
  127. package/src/node-direct.ts +37 -0
  128. package/src/reflection.ts +7 -0
  129. package/src/server/client-ip.ts +60 -0
  130. package/src/server/context.ts +50 -2
  131. package/src/server/contract.ts +26 -7
  132. package/src/server/http.ts +58 -0
  133. package/src/server/idempotency-lane.ts +287 -0
  134. package/src/server/install.ts +28 -0
  135. package/src/server/ledger-lane.ts +125 -0
  136. package/src/server/mcp-hook.ts +41 -0
  137. package/src/server/node-outcome-hook.ts +9 -0
  138. package/src/server/node-outcome.ts +112 -0
  139. package/src/server/plugin.ts +92 -0
  140. package/src/server/query.ts +119 -0
  141. package/src/server/registry.ts +36 -6
  142. package/src/server/request-context.ts +271 -0
  143. package/src/server/respond.ts +93 -0
  144. package/src/server/runtime-core.ts +74 -0
  145. package/src/server/server-types.ts +204 -0
  146. package/src/server/server.ts +402 -1314
  147. package/src/server/sse-hook.ts +14 -0
  148. package/src/server/sse.ts +26 -0
  149. package/src/server/websocket.ts +20 -0
  150. package/src/server/ws-hook.ts +8 -19
  151. package/src/server.ts +92 -0
  152. package/src/ws.ts +33 -10
  153. package/dist/invariants.d.ts +0 -70
  154. package/dist/invariants.d.ts.map +0 -1
  155. package/dist/invariants.js +0 -487
  156. package/dist/invariants.js.map +0 -1
  157. package/src/invariants.ts +0 -604
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Runtime-core: the small primitives shared across the server's request/response modules - the context
3
+ * symbols, the `ResponseResult` marker, the lazy never-abort signal / unbounded budget singletons, and
4
+ * the request-source accessors. Kept in one leaf (it only type-imports the server's spine types, never
5
+ * its values) so `request-context`, `respond`, `node-outcome`, and the kernel form a cycle-free graph.
6
+ */
7
+ import { createUnboundedRequestBudget, type RequestBudget } from "../budget.ts"
8
+ import type { CtxSet, RequestSource } from "./server.ts"
9
+
10
+ /** A handler returns a `Response` (used as-is) or any value (serialized to JSON). */
11
+ export type HandlerResult = Response | unknown
12
+
13
+ export const RESPONSE_RESULT = Symbol.for("nifra.response.result")
14
+ export const CONTEXT_SET = Symbol("nifra.context.set")
15
+ export const CONTEXT_SEARCH = Symbol("nifra.context.search")
16
+
17
+ export interface ResponseResult {
18
+ readonly [RESPONSE_RESULT]: true
19
+ toResponse(): Response
20
+ toNodeBody?(): {
21
+ readonly status: number
22
+ readonly headers: Readonly<Record<string, string | readonly string[]>> | undefined
23
+ readonly body: string | Uint8Array
24
+ }
25
+ }
26
+
27
+ export function isResponseResult(value: unknown): value is ResponseResult {
28
+ return (
29
+ typeof value === "object" &&
30
+ value !== null &&
31
+ (value as { readonly [RESPONSE_RESULT]?: unknown })[RESPONSE_RESULT] === true &&
32
+ typeof (value as { readonly toResponse?: unknown }).toResponse === "function"
33
+ )
34
+ }
35
+
36
+ /** The concrete `Request` for a source - itself when a real `Request` was passed (the Web path), or the
37
+ * lazily-built one (the Node adapter). A real `Request` IS a `RequestSource`, so no wrapper is allocated
38
+ * on the Web hot path. */
39
+ export function requestOf(source: RequestSource): Request {
40
+ return source.request ?? (source as unknown as Request)
41
+ }
42
+
43
+ export function headerOf(source: RequestSource, name: string): string | null {
44
+ return source.header?.(name) ?? source.headers.get(name)
45
+ }
46
+
47
+ /** Off-edge `waitUntil`: run the background work fire-and-forget, never leaking an unhandled
48
+ * rejection. Edge runtimes pass their own (Workers `ctx.waitUntil`) via the platform arg. */
49
+ export const fallbackWaitUntil = (promise: Promise<unknown>): void => {
50
+ void promise.catch(() => {})
51
+ }
52
+
53
+ // Finalization only reads `status`, `_headers`, and `_cookies`. The user-visible mutator methods
54
+ // live on `LazyResponseControls`, created by the `c.set` getter only when user code touches it.
55
+ export const EMPTY_RESPONSE_CONTROLS = Object.freeze({}) as CtxSet
56
+
57
+ export const TEXT_DECODER = new TextDecoder()
58
+
59
+ /**
60
+ * Shared never-aborting signal for `ctx.signal` when no timeout is armed - created lazily and
61
+ * cached. NOT a module-scope `new AbortController()`: edge runtimes (Cloudflare workerd) forbid
62
+ * constructing one in global scope; the first request builds it inside the handler, then it's
63
+ * reused at zero per-request cost.
64
+ */
65
+ let neverAbortSignal: AbortSignal | undefined
66
+ let unboundedRequestBudget: RequestBudget | undefined
67
+ export const getNeverAbortSignal = (): AbortSignal => {
68
+ neverAbortSignal ??= new AbortController().signal
69
+ return neverAbortSignal
70
+ }
71
+ export const getUnboundedRequestBudget = (): RequestBudget => {
72
+ unboundedRequestBudget ??= createUnboundedRequestBudget(getNeverAbortSignal())
73
+ return unboundedRequestBudget
74
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Public API-surface types for the server: construction options, the capacity-admission contract,
3
+ * route/tool/MCP descriptors, and the middleware bundle shape. Pure types (no runtime), split out of
4
+ * the server kernel so the descriptor/option vocabulary is one file, distinct from the engine.
5
+ */
6
+ import type { CapabilityUseEvent } from "../internal/capability-runtime.ts"
7
+ import type { AssuranceEvidence } from "../internal/route-assurance.ts"
8
+ import type { Method } from "../router/router.ts"
9
+ import type { ClientIpTrust } from "./client-ip.ts"
10
+ import type { Context, Platform, RouteSchema } from "./context.ts"
11
+ import type { Logger } from "./logger.ts"
12
+ import type { MaybePromise, OnRequestResult } from "./server.ts"
13
+
14
+ /**
15
+ * The outcome of a capacity-admission decision. `admitted` requests carry a `release` the server calls
16
+ * exactly once when the response is finalized; a shed request carries a ready `429` Response.
17
+ */
18
+ export type AdmissionDecision =
19
+ | { readonly admitted: true; release(): void }
20
+ | { readonly admitted: false; readonly response: Response }
21
+
22
+ /**
23
+ * A capacity-admission gate. Decides, per request, whether the instance has capacity to run it now -
24
+ * bounding *concurrency*, which rate limits (frequency) and deadlines (duration) do not. Provide an
25
+ * implementation (see `@nifrajs/middleware`'s `createAdmissionController`) as {@link ServerOptions.admission}.
26
+ *
27
+ * Enabling it trades the fused native-route lane for native matching + this gate (the gate cannot be a
28
+ * per-request `onRequest` hook without disabling native routes entirely); when unset, the request path
29
+ * is untouched.
30
+ */
31
+ export interface AdmissionController {
32
+ admit(req: Request): AdmissionDecision | Promise<AdmissionDecision>
33
+ }
34
+
35
+ export interface ServerOptions {
36
+ /**
37
+ * Max request body size (bytes), enforced **only when a route declares a body schema** - the cap
38
+ * lives in the schema-validated read path. Default 1_000_000.
39
+ *
40
+ * A route WITHOUT a body schema (raw body, file upload, BYO-validation) that reads `c.req` directly
41
+ * is not auto-bounded - use **`c.boundedBody(maxBytes?)`** / **`c.boundedJson(maxBytes?)`**, which
42
+ * apply this same cap (override per route by passing `maxBytes` - larger for an upload endpoint,
43
+ * smaller to tighten one).
44
+ */
45
+ readonly maxBodyBytes?: number
46
+ /** Max inbound WebSocket message size (bytes) when `listen()`ing on Bun - frames over this are rejected
47
+ * by the runtime before reaching your handler (so a huge frame can't be JSON-parsed into memory).
48
+ * Default: `maxBodyBytes` (1 MB). */
49
+ readonly wsMaxPayloadBytes?: number
50
+ /** Per-request timeout (ms): a slower request gets a 503 and `ctx.signal` aborts. 0 disables (default). */
51
+ readonly requestTimeoutMs?: number
52
+ /**
53
+ * How to derive `c.clientIp` behind a reverse proxy or CDN. Omit (default) to trust only the raw
54
+ * socket peer and never believe a forwarded header. Set `{ trustedHops: n }` when the app sits
55
+ * behind `n` proxies you operate (the caller is read from `X-Forwarded-For` past those hops), or
56
+ * `{ header: name }` to trust one edge-set header's first value. Declaring trust the app can't
57
+ * actually enforce lets clients forge their IP, so leave it unset unless a proxy really fronts you.
58
+ */
59
+ readonly clientIp?: ClientIpTrust
60
+ /**
61
+ * Admit the public `x-nifra-deadline` header and let it shorten local work. Disabled by default:
62
+ * services that participate in trusted cross-service deadline propagation opt in explicitly,
63
+ * while ordinary/public HTTP routes pay no header-admission tax and ignore hostile client values.
64
+ */
65
+ readonly acceptInboundDeadlines?: boolean
66
+ /**
67
+ * Maximum time accepted from an inbound `x-nifra-deadline`, in milliseconds. An incoming absolute
68
+ * deadline may shorten this cap (and `requestTimeoutMs`, when configured), never extend it.
69
+ * Default 30_000; applies only when `acceptInboundDeadlines` is enabled and the header is present.
70
+ */
71
+ readonly maxInboundDeadlineMs?: number
72
+ /** When `listen()`ing, install SIGTERM/SIGINT handlers that gracefully `stop()`. Default false. */
73
+ readonly gracefulSignals?: boolean
74
+ /**
75
+ * Capacity-admission gate (see {@link AdmissionController}). Bounds concurrent in-flight work so a
76
+ * healthy instance sheds load (`429` + `Retry-After`) instead of accepting more than it can finish.
77
+ * Off by default. Enabling it disables the fused native-route fast path (requests route through the
78
+ * shared matched lane where the gate runs); when unset, the request path pays nothing.
79
+ */
80
+ readonly admission?: AdmissionController
81
+ /**
82
+ * Optional token-only observation hook called by `useCapability(c, id)`. It receives no request,
83
+ * parameters, body, tenant, or values. Routes without capability declarations keep the old hot path.
84
+ */
85
+ readonly onCapabilityUse?: (event: CapabilityUseEvent) => void
86
+ /** Structured logger for framework events (redacts secrets). Default: JSON to stderr. */
87
+ readonly logger?: Logger
88
+ /**
89
+ * App-wide fallback fired when a route **without its own `onValidationError`** fails body/query
90
+ * validation. Same contract as the per-route hook (`(issues, ctx, kind) => Response | repaired-value |
91
+ * undefined`): a route's own hook takes precedence, and a route can fall through to the plain `422` by
92
+ * returning `undefined`. Use it for one app-wide error envelope (like tRPC's `errorFormatter` /
93
+ * Fastify's `setErrorHandler`) instead of repeating a formatter per route.
94
+ */
95
+ readonly onValidationError?: RouteSchema["onValidationError"]
96
+ }
97
+
98
+ /**
99
+ * MCP tool safety hints, surfaced in `tools/list`, that tell an agent how risky a `.tool()` call is - so it
100
+ * can decide whether to auto-invoke or confirm first. All optional; an omitted hint means "unknown". Mirrors
101
+ * the MCP spec's tool `annotations`.
102
+ */
103
+ export interface ToolAnnotations {
104
+ /** Human-readable display title for the tool, distinct from the machine `name`. */
105
+ readonly title?: string
106
+ /** The tool does not modify its environment (a pure read). */
107
+ readonly readOnlyHint?: boolean
108
+ /** The tool may perform destructive updates - only meaningful when `readOnlyHint` is not `true`. */
109
+ readonly destructiveHint?: boolean
110
+ /** Repeated calls with the same arguments have no additional effect beyond the first. */
111
+ readonly idempotentHint?: boolean
112
+ /** The tool interacts with external entities (an "open world" beyond this server). */
113
+ readonly openWorldHint?: boolean
114
+ }
115
+
116
+ /**
117
+ * A registered route's public descriptor - method, path, and input schemas. The
118
+ * router trie discards the original patterns, so this flat list is what lets tools
119
+ * (e.g. `toOpenAPI`) enumerate routes after registration.
120
+ */
121
+ export interface RouteDescriptor {
122
+ readonly method: Method
123
+ readonly path: string
124
+ readonly schema: RouteSchema | undefined
125
+ /** Effective enforcement evidence, populated only during reflection/introspection. */
126
+ readonly assurance?: readonly AssuranceEvidence[]
127
+ /** Normalized declared effect tokens, populated only when the route declares any. */
128
+ readonly capabilities?: readonly string[]
129
+ /** Set when the route is a declared dynamic route family (`schema.family`). */
130
+ readonly family?: boolean
131
+ readonly tool?: {
132
+ readonly name: string
133
+ readonly description: string
134
+ readonly annotations?: ToolAnnotations
135
+ }
136
+ }
137
+
138
+ /** A message in an MCP prompt's rendered output (see {@link Server.prompt}). */
139
+ export interface PromptMessage {
140
+ readonly role: "user" | "assistant"
141
+ readonly content: { readonly type: "text"; readonly text: string }
142
+ }
143
+
144
+ /** One declared argument of an MCP prompt, surfaced in `prompts/list`. */
145
+ export interface PromptArgument {
146
+ readonly name: string
147
+ readonly description?: string
148
+ readonly required?: boolean
149
+ }
150
+
151
+ /** An app-declared MCP resource - read-only data an agent can fetch through `nifra mcp`. */
152
+ export interface McpResourceDescriptor {
153
+ readonly uri: string
154
+ readonly name: string
155
+ readonly description?: string
156
+ readonly mimeType?: string
157
+ readonly read: () => MaybePromise<string | { readonly text: string; readonly mimeType?: string }>
158
+ }
159
+
160
+ /** An app-declared MCP prompt - a reusable prompt template an agent can fetch through `nifra mcp`. */
161
+ export interface McpPromptDescriptor {
162
+ readonly name: string
163
+ readonly description: string
164
+ readonly arguments?: readonly PromptArgument[]
165
+ readonly handler: (args: Record<string, string>) => MaybePromise<readonly PromptMessage[]>
166
+ }
167
+
168
+ /**
169
+ * The handle `listen()` returns - the slice of Bun's server nifra holds and exposes.
170
+ * Declared explicitly (rather than `ReturnType<typeof Bun.serve>`) so the public type
171
+ * surface doesn't leak the ambient `Bun` global into consumers' `.d.ts` resolution.
172
+ */
173
+ export interface RunningServer {
174
+ readonly port: number
175
+ readonly hostname: string
176
+ readonly pendingRequests: number
177
+ stop(closeActiveConnections?: boolean): void
178
+ }
179
+
180
+ /**
181
+ * A bundle of lifecycle hooks applied together via {@link Server.use} - the unit
182
+ * `@nifrajs/middleware` ships (cors, security headers, rate-limit). Every hook is
183
+ * optional and wired to its lifecycle point. Middleware is context-agnostic (sees
184
+ * the base `Context`); `use` does no context-type merging - the full type-merging
185
+ * plugin system is deferred, and `.use` is reserved as its future entry point.
186
+ */
187
+ export interface Middleware {
188
+ readonly name?: string
189
+ readonly onRequest?: (req: Request, platform?: Platform) => MaybePromise<OnRequestResult>
190
+ readonly around?: <T>(context: Context, next: () => MaybePromise<T>) => MaybePromise<T>
191
+ readonly beforeHandle?: (context: Context) => MaybePromise<unknown>
192
+ readonly afterHandle?: (result: unknown, context: Context) => MaybePromise<unknown>
193
+ readonly onResponse?: (response: Response, req: Request) => MaybePromise<Response>
194
+ readonly onResponseFinalized?: (outcome: ResponseFinalization, req: Request) => MaybePromise<void>
195
+ readonly onError?: (error: unknown, context: Context) => MaybePromise<unknown>
196
+ }
197
+
198
+ /** The terminal response-pipeline outcome observed after every transforming `onResponse` hook. */
199
+ export interface ResponseFinalization {
200
+ /** The final response, or the last response available when a transformation failed. */
201
+ readonly response: Response
202
+ /** A response-hook failure. Terminal observers run fail-open before the error is rethrown. */
203
+ readonly error?: unknown
204
+ }