@nifrajs/core 4.0.2 → 5.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 +6 -2
- package/dist/assurance.d.ts +71 -5
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +90 -25
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +52 -0
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +7 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- 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 +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.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/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- 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 +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +29 -5
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +44 -31
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +444 -321
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +22 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +21 -0
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-hook.d.ts +7 -1
- 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.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +20 -1
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +3 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +180 -31
- package/src/capabilities.ts +59 -0
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +7 -2
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +6 -1
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-program.ts +98 -44
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +3 -0
- package/src/server/context.ts +2 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +29 -5
- package/src/server/server.ts +659 -530
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +44 -5
- package/src/server/ws-hook.ts +5 -0
- package/src/server.ts +5 -0
- package/src/single-copy.ts +20 -1
- package/src/tool-contract.ts +67 -39
- package/src/ws.ts +4 -0
package/src/server/body.ts
CHANGED
|
@@ -194,6 +194,37 @@ function transportCapError(status: 400 | 413): Response {
|
|
|
194
194
|
return jsonError(status, status === 413 ? "payload_too_large" : "invalid_content_length")
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* A copy of `request` whose body errors with the transport cap's `413` once more than `maxBytes`
|
|
199
|
+
* arrive, or as soon as it is read when the declared Content-Length is over the cap (`413`) or
|
|
200
|
+
* malformed (`400`). The compact form of {@link applyTransportCap} for a server that hands the
|
|
201
|
+
* handler its own `Request` and has no identity to preserve; nothing is read until the handler reads.
|
|
202
|
+
*/
|
|
203
|
+
export function capBodyBytes(request: Request, maxBytes: number): Request {
|
|
204
|
+
const body = request.body
|
|
205
|
+
if (body === null) return request
|
|
206
|
+
const declared = request.headers.get("content-length")
|
|
207
|
+
const length = declared === null ? 0 : parseContentLength(declared)
|
|
208
|
+
let total = 0
|
|
209
|
+
const init: RequestInit & { duplex: "half" } = {
|
|
210
|
+
body: body.pipeThrough(
|
|
211
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
212
|
+
start(controller) {
|
|
213
|
+
if (length === undefined || length > maxBytes)
|
|
214
|
+
controller.error(transportCapError(length === undefined ? 400 : 413))
|
|
215
|
+
},
|
|
216
|
+
transform(chunk, controller) {
|
|
217
|
+
total += chunk.byteLength
|
|
218
|
+
if (total > maxBytes) controller.error(transportCapError(413))
|
|
219
|
+
else controller.enqueue(chunk)
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
duplex: "half",
|
|
224
|
+
}
|
|
225
|
+
return new Request(request, init)
|
|
226
|
+
}
|
|
227
|
+
|
|
197
228
|
/** First matching own-or-inherited descriptor, so instance shadows and prototype accessors both resolve. */
|
|
198
229
|
function descriptorOf(target: object, key: string): PropertyDescriptor | undefined {
|
|
199
230
|
let current: object | null = target
|
|
@@ -278,7 +309,9 @@ function cappedStreamOf(
|
|
|
278
309
|
}
|
|
279
310
|
total += value.byteLength
|
|
280
311
|
if (total > maxBytes) {
|
|
281
|
-
|
|
312
|
+
// Not awaited, as in drainCapped: on a teed body the cancel settles only once the other
|
|
313
|
+
// branch is read or cancelled too, and the 413 must not wait for an unread clone.
|
|
314
|
+
active.cancel().catch(() => {})
|
|
282
315
|
controller.error(reject(413))
|
|
283
316
|
return
|
|
284
317
|
}
|
package/src/server/client-ip.ts
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* - `{ header: name }` - trust a single named header's first value (e.g. a CDN that overwrites it at
|
|
23
23
|
* the edge). Only declare this when the app is reachable **exclusively** through that edge; a
|
|
24
24
|
* directly-reachable app lets any client forge the header.
|
|
25
|
+
*
|
|
26
|
+
* A hop count that is not a non-negative integer throws when the server is created; an invalid
|
|
27
|
+
* header name fails on the first request that reads `c.clientIp`.
|
|
25
28
|
*/
|
|
26
29
|
export type ClientIpTrust = { readonly trustedHops: number } | { readonly header: string }
|
|
27
30
|
|
package/src/server/context.ts
CHANGED
|
@@ -31,7 +31,8 @@ export interface IdempotencyConfig {
|
|
|
31
31
|
* Required server-owned key namespace. Use a tenant/subject-scoped opaque token for authenticated
|
|
32
32
|
* routes so two principals choosing the same header key never collide. A static namespace is an
|
|
33
33
|
* explicit shared/public scope and is safe only when the response is principal-independent. The
|
|
34
|
-
* resolver receives
|
|
34
|
+
* resolver receives the request's method, URL and headers, never its body, and must not derive
|
|
35
|
+
* identity from unauthenticated client input.
|
|
35
36
|
*/
|
|
36
37
|
readonly namespace:
|
|
37
38
|
| string
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* `server()` tree-shakes the whole dedupe machinery out.
|
|
5
5
|
*
|
|
6
6
|
* The kernel resolves a route's declared idempotency at registration (via {@link IdempotencyRuntime.resolve})
|
|
7
|
-
* and, on the request path, hands a matched idempotent route to {@link IdempotencyRuntime.run}.
|
|
8
|
-
*
|
|
7
|
+
* and, on the request path, hands a matched idempotent route to {@link IdempotencyRuntime.run}. Server
|
|
8
|
+
* execution is supplied through {@link IdempotencyHost}; the opt-in module adapts the matched
|
|
9
|
+
* route's program for replay without adding replay preparation to the shared server kernel.
|
|
9
10
|
*/
|
|
10
11
|
import { RouteConfigError } from "../errors.ts"
|
|
11
12
|
import {
|
|
@@ -28,6 +29,11 @@ import {
|
|
|
28
29
|
requestEffectEvidence,
|
|
29
30
|
requestIsSafeToRetry,
|
|
30
31
|
} from "../internal/effect-execution.ts"
|
|
32
|
+
import {
|
|
33
|
+
compileRouteExecutionPlan,
|
|
34
|
+
type InternalHandler,
|
|
35
|
+
type RouteEntry,
|
|
36
|
+
} from "../internal/route-execution.ts"
|
|
31
37
|
import { readBoundedBytes } from "./body.ts"
|
|
32
38
|
import type { Platform, RouteSchema } from "./context.ts"
|
|
33
39
|
import { jsonError, urlPartsOf } from "./http.ts"
|
|
@@ -42,6 +48,17 @@ const HANDLER_ENTERED = new WeakSet<Request>()
|
|
|
42
48
|
* owned effect begun, releases its key, so a refused caller cannot hold a store entry for the TTL. */
|
|
43
49
|
const REFUSALS = new Set([401, 403, 408, 409, 425, 429])
|
|
44
50
|
|
|
51
|
+
// Compile once in the opt-in plugin. The around runner also handles an empty hook list.
|
|
52
|
+
const REPLAY_EXECUTION = compileRouteExecutionPlan({
|
|
53
|
+
lane: "lifecycle",
|
|
54
|
+
contextless: false,
|
|
55
|
+
hasAround: true,
|
|
56
|
+
hasLedger: false,
|
|
57
|
+
fusedWeb: undefined,
|
|
58
|
+
fusedBody: undefined,
|
|
59
|
+
fusedLane: undefined,
|
|
60
|
+
})
|
|
61
|
+
|
|
45
62
|
/** Registration-resolved idempotency for a route: the config with its store + defaults pinned. */
|
|
46
63
|
export interface ResolvedIdempotency {
|
|
47
64
|
readonly store: IdempotencyStore
|
|
@@ -55,7 +72,8 @@ export interface ResolvedIdempotency {
|
|
|
55
72
|
|
|
56
73
|
/**
|
|
57
74
|
* What the dedupe lane needs from the server to run a fresh key: the app-wide body cap and a way to
|
|
58
|
-
* run the buffered request through the route's normal lanes to a concrete `Response`.
|
|
75
|
+
* run the buffered request through the route's normal lanes to a concrete `Response`. The replay
|
|
76
|
+
* path supplies an entry with the stored response replacing the handler and stored transformations.
|
|
59
77
|
*/
|
|
60
78
|
export interface IdempotencyHost {
|
|
61
79
|
readonly maxBodyBytes: number
|
|
@@ -65,7 +83,7 @@ export interface IdempotencyHost {
|
|
|
65
83
|
entry: unknown,
|
|
66
84
|
params: Record<string, string>,
|
|
67
85
|
search: string | undefined,
|
|
68
|
-
): Promise<Response>
|
|
86
|
+
): Response | Promise<Response>
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/** The injected idempotency implementation the kernel calls through when the plugin is installed. */
|
|
@@ -101,7 +119,9 @@ interface IdempotencyInstallable {
|
|
|
101
119
|
|
|
102
120
|
/**
|
|
103
121
|
* Enable request idempotency. Routes that declare `schema.idempotency` get the dedupe lane: a repeat
|
|
104
|
-
* `Idempotency-Key`
|
|
122
|
+
* `Idempotency-Key` rechecks the current route prerequisites (authentication, validation and policy
|
|
123
|
+
* hooks) before replaying the stored response. The handler, effect ledger and stored response
|
|
124
|
+
* transformations run only for the fresh request. Without this plugin,
|
|
105
125
|
* declaring `schema.idempotency` is a registration error (the safety gate can never be silently
|
|
106
126
|
* dropped). `store` is the app-wide default backing routes that do not pin their own; omit it for a
|
|
107
127
|
* shared in-process store (development/tests only - inject a durable store in production).
|
|
@@ -231,7 +251,10 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
231
251
|
}
|
|
232
252
|
const namespace =
|
|
233
253
|
typeof config.namespace === "function"
|
|
234
|
-
? await config.namespace(
|
|
254
|
+
? await config.namespace(
|
|
255
|
+
{ method: req.method, url: req.url, headers: req.headers },
|
|
256
|
+
platform,
|
|
257
|
+
)
|
|
235
258
|
: config.namespace
|
|
236
259
|
if (!validIdempotencyNamespace(namespace)) {
|
|
237
260
|
return wrapResponse(jsonError(500, "idempotency_namespace_invalid"))
|
|
@@ -250,6 +273,13 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
250
273
|
canonicalBody,
|
|
251
274
|
req.headers.get("content-type") ?? "",
|
|
252
275
|
)
|
|
276
|
+
const buffered = new Request(req.url, {
|
|
277
|
+
method: req.method,
|
|
278
|
+
headers: req.headers,
|
|
279
|
+
signal: req.signal,
|
|
280
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: readBoundedBytes owns this byte view; Request accepts the same bytes on every supported runtime.
|
|
281
|
+
body: read.bytes.byteLength ? (read.bytes as NonNullable<RequestInit["body"]>) : null,
|
|
282
|
+
})
|
|
253
283
|
|
|
254
284
|
const pendingTtlMs = config.pendingTtlMs
|
|
255
285
|
const begin = await config.store.begin({
|
|
@@ -259,11 +289,6 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
259
289
|
ttlMs: config.ttlMs,
|
|
260
290
|
pendingTtlMs,
|
|
261
291
|
})
|
|
262
|
-
if (begin.state === "replay") {
|
|
263
|
-
return wrapResponse(
|
|
264
|
-
responseFromStored(begin.response, { maxBytes: config.maxResponseBytes }),
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
292
|
if (begin.state === "mismatch") return wrapResponse(jsonError(409, "idempotency_key_reused"))
|
|
268
293
|
if (begin.state === "in-flight") {
|
|
269
294
|
return wrapResponse(jsonError(409, "idempotency_in_progress", { "Retry-After": "1" }))
|
|
@@ -271,6 +296,21 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
271
296
|
if (begin.state === "capacity") {
|
|
272
297
|
return wrapResponse(jsonError(503, "idempotency_store_capacity", { "Retry-After": "1" }))
|
|
273
298
|
}
|
|
299
|
+
beginRequestEffectTracking(buffered)
|
|
300
|
+
if (begin.state === "replay") {
|
|
301
|
+
return wrapResponse(
|
|
302
|
+
await host.runLanes(
|
|
303
|
+
buffered,
|
|
304
|
+
platform,
|
|
305
|
+
replayEntry(
|
|
306
|
+
entry,
|
|
307
|
+
responseFromStored(begin.response, { maxBytes: config.maxResponseBytes }),
|
|
308
|
+
),
|
|
309
|
+
params,
|
|
310
|
+
search,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
}
|
|
274
314
|
const reservation = begin.reservation
|
|
275
315
|
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
276
316
|
if (pendingTtlMs !== undefined) {
|
|
@@ -286,17 +326,6 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
286
326
|
;(heartbeat as { unref?: () => void }).unref?.()
|
|
287
327
|
}
|
|
288
328
|
try {
|
|
289
|
-
// Fresh key: re-expose the buffered body as a Request so the normal lanes can read + validate it,
|
|
290
|
-
// then run those lanes to a concrete Response.
|
|
291
|
-
const bufferedInit: RequestInit = {
|
|
292
|
-
method: req.method,
|
|
293
|
-
headers: req.headers,
|
|
294
|
-
signal: req.signal,
|
|
295
|
-
}
|
|
296
|
-
if (read.bytes.byteLength > 0)
|
|
297
|
-
bufferedInit.body = read.bytes as NonNullable<RequestInit["body"]>
|
|
298
|
-
const buffered = new Request(req.url, bufferedInit)
|
|
299
|
-
beginRequestEffectTracking(buffered)
|
|
300
329
|
// The handler threw, or its response body failed while being captured: no response to store.
|
|
301
330
|
const settleThrown = async (err: unknown): Promise<ReturnType<typeof wrapResponse>> => {
|
|
302
331
|
if (!requestEffectEvidence(buffered).began) {
|
|
@@ -394,3 +423,16 @@ export function createIdempotencyRuntime(options?: IdempotencyPluginOptions): Id
|
|
|
394
423
|
},
|
|
395
424
|
}
|
|
396
425
|
}
|
|
426
|
+
|
|
427
|
+
function replayEntry(entry: unknown, response: Response): RouteEntry {
|
|
428
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the kernel supplies its matched RouteEntry through this internal opaque install seam.
|
|
429
|
+
const route = entry as RouteEntry
|
|
430
|
+
const handler: InternalHandler = () => response
|
|
431
|
+
return {
|
|
432
|
+
...route,
|
|
433
|
+
handler,
|
|
434
|
+
responseContract: undefined,
|
|
435
|
+
program: { ...route.program, handler, afterHandle: [] },
|
|
436
|
+
execution: REPLAY_EXECUTION,
|
|
437
|
+
}
|
|
438
|
+
}
|
package/src/server/install.ts
CHANGED
|
@@ -25,6 +25,9 @@ export const INSTALL_SSE: unique symbol = Symbol.for("@nifrajs/core/install-sse"
|
|
|
25
25
|
/** @internal Install the Node-direct renderer on a server (called by the `nodeDirect()` plugin). */
|
|
26
26
|
export const INSTALL_NODE_DIRECT: unique symbol = Symbol.for("@nifrajs/core/install-node-direct")
|
|
27
27
|
|
|
28
|
+
/** @internal Log an error an adapter caught past `fetch`, through the app's own logger. */
|
|
29
|
+
export const REPORT_ERROR: unique symbol = Symbol.for("@nifrajs/core/report-error")
|
|
30
|
+
|
|
28
31
|
/** @internal Install the not-found lane on a server (called by the `notFound()` plugin). */
|
|
29
32
|
export const INSTALL_NOT_FOUND: unique symbol = Symbol.for("@nifrajs/core/install-not-found")
|
|
30
33
|
|
package/src/server/logger.ts
CHANGED
|
@@ -57,7 +57,9 @@ export interface RedactOptions {
|
|
|
57
57
|
export const commonSecretPatterns: ReadonlyArray<RegExp> = [
|
|
58
58
|
/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, // Authorization: Bearer <token>
|
|
59
59
|
/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, // JWT (header.payload.signature)
|
|
60
|
-
|
|
60
|
+
// Email. The lookbehind starts a match only where a run of address characters starts; `\b` would
|
|
61
|
+
// restart inside `a.a.a.` at every `a` and rescan the run each time, quadratic in its length.
|
|
62
|
+
/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
61
63
|
/\b[sprk]k_(?:live|test)_[A-Za-z0-9]{8,}/g, // Stripe-style secret/restricted keys
|
|
62
64
|
/\bgh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub personal/OAuth/refresh tokens
|
|
63
65
|
/\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
|
package/src/server/methods.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { MethodRoutesPlugin } from "./plugin.ts"
|
|
|
9
9
|
import type { OutputOf } from "./registry.ts"
|
|
10
10
|
import type { Handler } from "./server.ts"
|
|
11
11
|
|
|
12
|
+
export { isRegistrableMethod } from "../router/router.ts"
|
|
12
13
|
export type { MethodRoutesPlugin } from "./plugin.ts"
|
|
13
14
|
|
|
14
15
|
/** The part of a server these plugins use: one atomic batch registration. */
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* plain-data render straight to the socket. Imports only runtime-core + respond + the spine types.
|
|
5
5
|
*/
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
applySetToResponse,
|
|
8
8
|
applyStaticResponseHeaders,
|
|
9
9
|
markTaggedResponse,
|
|
10
10
|
normalizeBodylessResponse,
|
|
11
|
-
|
|
11
|
+
RESPONSE_OWNED_HEADERS,
|
|
12
|
+
rememberRenderedResponse,
|
|
13
|
+
SENT_BODY,
|
|
12
14
|
taggedResponseBody,
|
|
13
15
|
} from "./respond.ts"
|
|
14
16
|
import {
|
|
@@ -72,16 +74,17 @@ export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutc
|
|
|
72
74
|
return {
|
|
73
75
|
kind: "body",
|
|
74
76
|
status: body.status,
|
|
75
|
-
headers:
|
|
77
|
+
headers: applySetToNodeHeaders(body.headers, body.status, set),
|
|
76
78
|
body: isBodylessStatus(body.status) ? new Uint8Array(0) : body.body,
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
return nodeOutcomeFromResponse(
|
|
80
|
-
|
|
82
|
+
applySetToResponse(normalizeBodylessResponse(result.toResponse()), set),
|
|
81
83
|
)
|
|
82
84
|
}
|
|
83
85
|
if (result instanceof Response) {
|
|
84
|
-
|
|
86
|
+
if (result.bodyUsed) return plainNodeOutcome(SENT_BODY.plain!, EMPTY_RESPONSE_CONTROLS)
|
|
87
|
+
return nodeOutcomeFromResponse(applySetToResponse(normalizeBodylessResponse(result), set))
|
|
85
88
|
}
|
|
86
89
|
const status = set.status ?? (result === undefined ? 204 : 200)
|
|
87
90
|
const body = result === undefined || isBodylessStatus(status) ? null : JSON.stringify(result)
|
|
@@ -152,8 +155,7 @@ export function nodeOutcomeToResponse(outcome: NodeServeOutcome): Response {
|
|
|
152
155
|
headers,
|
|
153
156
|
})
|
|
154
157
|
if (body !== null) markTaggedResponse(response, body)
|
|
155
|
-
|
|
156
|
-
return response
|
|
158
|
+
return rememberRenderedResponse(response, body !== null)
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/**
|
|
@@ -232,13 +234,33 @@ function responseHeadersForNode(
|
|
|
232
234
|
return headers
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
/** {@link applySetToResponse} for a body the node lane writes from a header record. */
|
|
238
|
+
function applySetToNodeHeaders(
|
|
236
239
|
headers: Readonly<Record<string, string | readonly string[]>> | undefined,
|
|
237
|
-
|
|
240
|
+
status: number,
|
|
241
|
+
set: CtxSet,
|
|
238
242
|
): Readonly<Record<string, string | readonly string[]>> | undefined {
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
243
|
+
const ambient = status >= 200 ? set._headers : undefined
|
|
244
|
+
const cookies = set._cookies
|
|
245
|
+
const copy = (): Record<string, string | readonly string[]> =>
|
|
246
|
+
Object.assign(Object.create(null) as Record<string, string | readonly string[]>, headers)
|
|
247
|
+
let out: Record<string, string | readonly string[]> | undefined
|
|
248
|
+
if (ambient !== undefined) {
|
|
249
|
+
let own: Set<string> | undefined
|
|
250
|
+
for (const name in ambient) {
|
|
251
|
+
const lower = name.toLowerCase()
|
|
252
|
+
if (RESPONSE_OWNED_HEADERS.has(lower)) continue
|
|
253
|
+
own ??= new Set(
|
|
254
|
+
headers === undefined ? [] : Object.keys(headers).map((key) => key.toLowerCase()),
|
|
255
|
+
)
|
|
256
|
+
if (own.has(lower)) continue
|
|
257
|
+
own.add(lower)
|
|
258
|
+
out ??= copy()
|
|
259
|
+
out[name] = ambient[name]!
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (cookies === undefined || cookies.length === 0) return out ?? headers
|
|
263
|
+
out ??= copy()
|
|
242
264
|
const existing = out["set-cookie"]
|
|
243
265
|
const setCookies =
|
|
244
266
|
existing === undefined ? [] : typeof existing === "string" ? [existing] : [...existing]
|
|
@@ -67,8 +67,9 @@ function absoluteUrl(
|
|
|
67
67
|
// Fall through to the structured event fields.
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
70
|
+
const url = new URL(`https://${headers.get("host") ?? "nifra.invalid"}`)
|
|
71
|
+
// Assigned, not resolved against the origin: resolving reads `//evil.example/admin` as a host.
|
|
72
|
+
url.pathname = path || "/"
|
|
72
73
|
if (query !== undefined && query !== "") url.search = query.startsWith("?") ? query : `?${query}`
|
|
73
74
|
return url.toString()
|
|
74
75
|
}
|
|
@@ -15,6 +15,7 @@ import type { ProtoPoisoning } from "./proto-guard.ts"
|
|
|
15
15
|
import { searchOf } from "./query.ts"
|
|
16
16
|
import { responseJsonContentType } from "./respond.ts"
|
|
17
17
|
import {
|
|
18
|
+
CONTEXT_REQUEST,
|
|
18
19
|
CONTEXT_SEARCH,
|
|
19
20
|
CONTEXT_SET,
|
|
20
21
|
fallbackWaitUntil,
|
|
@@ -256,6 +257,10 @@ export class RequestContext implements RawContext {
|
|
|
256
257
|
return this.setValue
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
get [CONTEXT_REQUEST](): Request {
|
|
261
|
+
return requestOf(this.source)
|
|
262
|
+
}
|
|
263
|
+
|
|
259
264
|
get [CONTEXT_SEARCH](): string {
|
|
260
265
|
this.searchValue ??= searchOf(this.source.url)
|
|
261
266
|
return this.searchValue
|