@daloyjs/core 0.35.1 → 0.36.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 +1 -0
- package/dist/adapters/node.d.ts +11 -0
- package/dist/adapters/node.js +90 -28
- package/dist/app.d.ts +113 -0
- package/dist/app.js +372 -96
- package/dist/errors.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/safe-redirect.d.ts +2 -2
- package/dist/safe-redirect.js +2 -2
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +4 -19
- package/dist/security.js +36 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -466,6 +466,7 @@ The framework refuses to start (or to construct) when configuration is unsafe:
|
|
|
466
466
|
|
|
467
467
|
- Weak session secrets, `cors({ origin: "*" })` with credentials, `session()` + state-changing route without `csrf()`, and unconfigured `X-Forwarded-*` in production.
|
|
468
468
|
- `secureDefaults: false` in production unless `acknowledgeInsecureDefaults: true` is set, plus a once-per-process `error` log naming every disabled default.
|
|
469
|
+
- `preset: "internal-service"` topology preset for service-to-service deployments behind a mesh / sidecar / private network: turns OFF the browser-only guards (auto `secureHeaders`, `corsCrossOriginGuard`, `csrf` boot guard, unconfigured `X-Forwarded-*` guard) while keeping every input, parser, credential, SSRF, weak-secret, and refuse-to-boot guard ON. Per-knob options still win, the choice is logged at boot under `event: "security.preset.applied"`, and the live posture is auditable via `app.getSecurityPosture()`.
|
|
469
470
|
- `createJwtSigner()` / `createJwtVerifier()` refuse `alg: "none"`, accept only an explicit allowlist, refuse HS + JWK combinations, refuse to sign without `exp`, and refuse HS-shaped secrets under 32 bytes (RFC 7518 §3.2).
|
|
470
471
|
- `secureHeaders()` refuses to construct with `frameOptions: false` AND no CSP `frame-ancestors` directive (no clickjacking defense).
|
|
471
472
|
- `cors()` refuses `methods: ['*']` at construction; default `allowMethods` narrowed to `[GET, HEAD, POST]` so `PUT` / `PATCH` / `DELETE` become explicit opt-ins.
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -23,6 +23,17 @@ export interface NodeServerOptions {
|
|
|
23
23
|
* clients can spoof the scheme/host. Default: false.
|
|
24
24
|
*/
|
|
25
25
|
trustProxy?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum declared `Content-Length` (in bytes) for which the Node adapter
|
|
28
|
+
* pre-buffers the request body into a `Uint8Array` before constructing the
|
|
29
|
+
* `Request`. Bodies above this threshold fall back to the streaming
|
|
30
|
+
* `Readable.toWeb(req)` path so the adapter never holds an unbounded buffer
|
|
31
|
+
* per in-flight request — important under high concurrency where N
|
|
32
|
+
* simultaneous large uploads would otherwise pin N × threshold bytes of
|
|
33
|
+
* memory. The threshold is independently capped by `App.bodyLimitBytes`,
|
|
34
|
+
* which is the actual security limit. Default: 256 KiB.
|
|
35
|
+
*/
|
|
36
|
+
bufferedBodyMaxBytes?: number;
|
|
26
37
|
}
|
|
27
38
|
/** Handle returned by {@link serve} exposing the underlying Node `Server` plus a `close()` for graceful shutdown. */
|
|
28
39
|
export interface NodeServerHandle {
|
package/dist/adapters/node.js
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createServer, } from "node:http";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
|
-
import { DALOY_RAW_BODY } from "../app.js";
|
|
7
|
+
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY } from "../app.js";
|
|
8
8
|
import { FrameSink, encodeFrame, encodeClosePayload, encodeSendPayload, validateUpgrade, validateSelectedSubprotocol, checkWebSocketOrigin, WS_OPCODE, WS_CLOSE_CODE, WS_READY_STATE, WS_MAX_CONTROL_PAYLOAD, WebSocketProtocolError, WebSocketPayloadTooLargeError, } from "../websocket.js";
|
|
9
9
|
/** Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}. */
|
|
10
10
|
export function serve(app, opts = {}) {
|
|
11
11
|
const trustProxy = opts.trustProxy === true;
|
|
12
|
+
const bufferedBodyMaxBytes = typeof opts.bufferedBodyMaxBytes === "number" && opts.bufferedBodyMaxBytes >= 0
|
|
13
|
+
? opts.bufferedBodyMaxBytes
|
|
14
|
+
: DEFAULT_BUFFERED_BODY_MAX_BYTES;
|
|
12
15
|
const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, (req, res) => {
|
|
13
16
|
// GET/HEAD: no body work, dispatch directly. Keep this first so the GET
|
|
14
17
|
// hot path doesn't pay for any of the buffering bookkeeping below.
|
|
@@ -23,7 +26,7 @@ export function serve(app, opts = {}) {
|
|
|
23
26
|
// WHATWG-stream adapter that dominates POST throughput on Node.
|
|
24
27
|
const cl = req.headers["content-length"];
|
|
25
28
|
const n = cl ? Number(cl) : NaN;
|
|
26
|
-
if (Number.isFinite(n) && n >= 0 && n <=
|
|
29
|
+
if (Number.isFinite(n) && n >= 0 && n <= bufferedBodyMaxBytes) {
|
|
27
30
|
bufferRequestBody(req, n).then((bytes) => dispatchToApp(app, req, res, trustProxy, bytes), (e) => writeAdapterError(res, e));
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
@@ -70,12 +73,14 @@ export function serve(app, opts = {}) {
|
|
|
70
73
|
return { server, port, close };
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* Default pre-buffer ceiling for the Node adapter. 256 KiB is a compromise:
|
|
77
|
+
* large enough that the vast majority of JSON / form requests stay on the
|
|
78
|
+
* fast (Uint8Array) path, small enough that N concurrent in-flight bodies
|
|
79
|
+
* don't pin huge amounts of memory. Override via
|
|
80
|
+
* {@link NodeServerOptions.bufferedBodyMaxBytes}. The actual security cap
|
|
81
|
+
* on body size remains `App.bodyLimitBytes`.
|
|
77
82
|
*/
|
|
78
|
-
const
|
|
83
|
+
const DEFAULT_BUFFERED_BODY_MAX_BYTES = 256 * 1024;
|
|
79
84
|
function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
|
|
80
85
|
let request;
|
|
81
86
|
try {
|
|
@@ -111,21 +116,31 @@ function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
|
|
|
111
116
|
}
|
|
112
117
|
function bufferRequestBody(req, expected) {
|
|
113
118
|
return new Promise((resolve, reject) => {
|
|
114
|
-
|
|
119
|
+
// Pre-allocate to the declared Content-Length. The caller has already
|
|
120
|
+
// checked `expected <= BUFFERED_BODY_MAX_BYTES` (1 MiB) so this
|
|
121
|
+
// allocation is bounded and DoS-safe. Skipping the intermediate
|
|
122
|
+
// `chunks: Buffer[]` array + `Buffer.concat` avoids one full-body
|
|
123
|
+
// copy per request — significant at 1 MiB bodies under load.
|
|
124
|
+
// Use `Buffer.alloc` (zero-filled) rather than `Buffer.allocUnsafe`:
|
|
125
|
+
// the unsafe variant returns uninitialized memory and is forbidden by
|
|
126
|
+
// `verify:no-unsafe-buffer`. Any unwritten tail is sliced off below.
|
|
127
|
+
const out = expected > 0 ? Buffer.alloc(expected) : null;
|
|
115
128
|
let received = 0;
|
|
116
129
|
let settled = false;
|
|
117
130
|
const onData = (chunk) => {
|
|
118
131
|
if (settled)
|
|
119
132
|
return;
|
|
120
|
-
received
|
|
121
|
-
if (
|
|
133
|
+
const next = received + chunk.length;
|
|
134
|
+
if (next > expected) {
|
|
122
135
|
settled = true;
|
|
123
136
|
cleanup();
|
|
124
137
|
req.destroy();
|
|
125
138
|
reject(new Error("Request body exceeded declared Content-Length"));
|
|
126
139
|
return;
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
// out is non-null here because next > 0 implies expected > 0.
|
|
142
|
+
chunk.copy(out, received);
|
|
143
|
+
received = next;
|
|
129
144
|
};
|
|
130
145
|
const onEnd = () => {
|
|
131
146
|
if (settled)
|
|
@@ -136,7 +151,9 @@ function bufferRequestBody(req, expected) {
|
|
|
136
151
|
resolve(new Uint8Array(0));
|
|
137
152
|
return;
|
|
138
153
|
}
|
|
139
|
-
|
|
154
|
+
// Trust Content-Length: if the client under-delivered we still
|
|
155
|
+
// resolve the prefix actually received (matches prior behavior).
|
|
156
|
+
const buf = received === expected ? out : out.subarray(0, received);
|
|
140
157
|
resolve(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
141
158
|
};
|
|
142
159
|
const onErr = (err) => {
|
|
@@ -184,23 +201,40 @@ function toWebRequest(req, trustProxy, bufferedBody) {
|
|
|
184
201
|
const proto = forwardedProto ??
|
|
185
202
|
(req.socket.encrypted ? "https" : "http");
|
|
186
203
|
const url = `${proto}://${host}${req.url ?? "/"}`;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
// Build headers from `rawHeaders` (a flat [k0,v0,k1,v1,...] array) instead
|
|
205
|
+
// of the parsed `req.headers` object. This matches @hono/node-server's
|
|
206
|
+
// `newHeadersFromIncoming`: one `new Headers([[k,v],...])` constructor
|
|
207
|
+
// call rather than N `headers.set()` calls. It is also stricter for
|
|
208
|
+
// duplicate-Host smuggling — Node coalesces some singleton headers down
|
|
209
|
+
// to the first value on `req.headers`, but `rawHeaders` preserves every
|
|
210
|
+
// occurrence so `assertNoDuplicateSingletonHeaders` actually sees them.
|
|
211
|
+
// Skip HTTP/2 pseudo-headers (leading ':') defensively, even though
|
|
212
|
+
// node:http's createServer is HTTP/1.1 only today.
|
|
213
|
+
const rawHeaders = req.rawHeaders;
|
|
214
|
+
const headerPairs = [];
|
|
215
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
216
|
+
const k = rawHeaders[i];
|
|
217
|
+
if (k.charCodeAt(0) === 58 /* ':' */)
|
|
191
218
|
continue;
|
|
192
|
-
|
|
219
|
+
headerPairs.push([k, rawHeaders[i + 1]]);
|
|
193
220
|
}
|
|
221
|
+
const headers = new Headers(headerPairs);
|
|
194
222
|
const method = req.method ?? "GET";
|
|
195
223
|
if (method === "GET" || method === "HEAD") {
|
|
196
224
|
return new Request(url, { method, headers });
|
|
197
225
|
}
|
|
198
226
|
if (bufferedBody !== undefined) {
|
|
199
|
-
|
|
227
|
+
const req2 = new Request(url, {
|
|
200
228
|
method,
|
|
201
229
|
headers,
|
|
202
230
|
body: bufferedBody,
|
|
203
231
|
});
|
|
232
|
+
// Stash the validated bytes so readBodyLimited (and any other internal
|
|
233
|
+
// body reader) can skip the WHATWG ReadableStream reader loop. The
|
|
234
|
+
// adapter has already enforced BUFFERED_BODY_MAX_BYTES + Content-Length
|
|
235
|
+
// here; readBodyLimited re-checks against the caller's limit.
|
|
236
|
+
req2[DALOY_REQUEST_RAW_BODY] = bufferedBody;
|
|
237
|
+
return req2;
|
|
204
238
|
}
|
|
205
239
|
return new Request(url, {
|
|
206
240
|
method,
|
|
@@ -234,6 +268,30 @@ function sendWebResponse(res, out) {
|
|
|
234
268
|
}
|
|
235
269
|
return;
|
|
236
270
|
}
|
|
271
|
+
// Fast-path: handler returned a raw stream. Check before `!res.body` —
|
|
272
|
+
// a Node Readable is stashed alongside a null Response body, and the
|
|
273
|
+
// `new Response(null)` constructor also auto-sets `content-length: 0`
|
|
274
|
+
// which we must strip so Node falls back to chunked transfer-encoding.
|
|
275
|
+
const rawStream = res[DALOY_RAW_STREAM];
|
|
276
|
+
if (rawStream !== undefined) {
|
|
277
|
+
if (typeof rawStream.pipe === "function" && !(rawStream instanceof ReadableStream)) {
|
|
278
|
+
// Node `Readable` from the handler: skip the Web-stream bridge entirely
|
|
279
|
+
// and `.pipe(out)` like Fastify/Koa/Express do.
|
|
280
|
+
out.removeHeader("content-length");
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const r = rawStream;
|
|
283
|
+
const onError = (err) => {
|
|
284
|
+
r.destroy();
|
|
285
|
+
reject(err);
|
|
286
|
+
};
|
|
287
|
+
r.once("error", onError);
|
|
288
|
+
out.once("error", onError);
|
|
289
|
+
out.once("finish", () => resolve());
|
|
290
|
+
r.pipe(out);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return pumpBody(rawStream, out);
|
|
294
|
+
}
|
|
237
295
|
if (!res.body) {
|
|
238
296
|
out.end();
|
|
239
297
|
return;
|
|
@@ -253,16 +311,20 @@ function sendWebResponse(res, out) {
|
|
|
253
311
|
}
|
|
254
312
|
return pumpBody(res.body, out);
|
|
255
313
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
314
|
+
function pumpBody(body, out) {
|
|
315
|
+
// Delegate to Node's native pipe: it honors backpressure and avoids the
|
|
316
|
+
// per-chunk microtask overhead of an explicit `await reader.read()` loop.
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const readable = Readable.fromWeb(body);
|
|
319
|
+
const onError = (err) => {
|
|
320
|
+
readable.destroy();
|
|
321
|
+
reject(err);
|
|
322
|
+
};
|
|
323
|
+
readable.once("error", onError);
|
|
324
|
+
out.once("error", onError);
|
|
325
|
+
out.once("finish", () => resolve());
|
|
326
|
+
readable.pipe(out);
|
|
327
|
+
});
|
|
266
328
|
}
|
|
267
329
|
// ---------- WebSocket upgrade ----------
|
|
268
330
|
async function handleUpgrade(app, req, socket, head, trustProxy) {
|
package/dist/app.d.ts
CHANGED
|
@@ -10,6 +10,33 @@ import { type BehindProxyConfig } from "./conn-info.js";
|
|
|
10
10
|
export declare function _resetCrashHandlersForTests(): void;
|
|
11
11
|
/** @internal Test-only helper to reset the latch between tests. */
|
|
12
12
|
export declare function _resetInsecureDefaultsLogForTests(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Named security posture preset. Currently only one value is supported:
|
|
15
|
+
*
|
|
16
|
+
* - `"internal-service"` — relaxes the *topology-dependent* defaults that
|
|
17
|
+
* only make sense when an HTTP boundary faces a browser or the public
|
|
18
|
+
* internet (auto `secureHeaders`, cross-origin write guard, the
|
|
19
|
+
* session+state-changing-route CSRF boot guard, and the unconfigured
|
|
20
|
+
* `X-Forwarded-*` guard). Everything that protects the service from
|
|
21
|
+
* malformed input, confused dependencies, or compromised callers —
|
|
22
|
+
* body limits, request timeouts, JWT algorithm allowlists, weak-secret
|
|
23
|
+
* refuse-to-boot, `cors({ origin: '*' })` refuse-to-boot, anonymous
|
|
24
|
+
* stateful plugin refuse-to-boot, `crashOnUnhandledRejection`, schema
|
|
25
|
+
* strictness, prototype-pollution-safe parsers, SSRF-safe `fetchGuard`
|
|
26
|
+
* defaults, RFC 9457 problem+json redaction — stays on. Per-knob
|
|
27
|
+
* options still win (`secureHeaders: { ... }` re-enables it on top of
|
|
28
|
+
* the preset). The preset choice is logged once at boot under the
|
|
29
|
+
* `security.preset.applied` event so operators can audit the posture
|
|
30
|
+
* without reading code.
|
|
31
|
+
*
|
|
32
|
+
* Topology presets are intentionally a small, curated set — they are NOT
|
|
33
|
+
* a master "disable everything" knob. If you really need to disable the
|
|
34
|
+
* entire secure-by-default surface, use the explicit
|
|
35
|
+
* {@link AppOptions.secureDefaults} `false` escape hatch.
|
|
36
|
+
*
|
|
37
|
+
* @since 0.34.0
|
|
38
|
+
*/
|
|
39
|
+
export type SecurityPreset = "internal-service";
|
|
13
40
|
/**
|
|
14
41
|
* Configuration accepted by {@link App}'s constructor. Every field is
|
|
15
42
|
* optional; sensible production defaults are applied.
|
|
@@ -21,6 +48,23 @@ export interface AppOptions {
|
|
|
21
48
|
title?: string;
|
|
22
49
|
version?: string;
|
|
23
50
|
description?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Topology-aware security posture preset. See {@link SecurityPreset}.
|
|
53
|
+
*
|
|
54
|
+
* - `"internal-service"` — for service-to-service deployments behind a
|
|
55
|
+
* service mesh, sidecar, or private network. Turns off the
|
|
56
|
+
* browser-/edge-only guards (auto `secureHeaders`, cross-origin write
|
|
57
|
+
* guard, session+state-changing CSRF boot guard, unconfigured
|
|
58
|
+
* `X-Forwarded-*` guard) while keeping every input-, parser-,
|
|
59
|
+
* credential-, and SSRF-level guard on. The choice is logged once at
|
|
60
|
+
* boot under the `security.preset.applied` event. Per-knob options
|
|
61
|
+
* you pass alongside the preset still win.
|
|
62
|
+
*
|
|
63
|
+
* Omit (default) for browser-facing / public APIs.
|
|
64
|
+
*
|
|
65
|
+
* @since 0.34.0
|
|
66
|
+
*/
|
|
67
|
+
preset?: SecurityPreset;
|
|
24
68
|
/** Validate handler responses against declared response schemas. Default: true. */
|
|
25
69
|
validateResponses?: boolean;
|
|
26
70
|
/** Hard cap on request body size in bytes. Default: 1 MiB. */
|
|
@@ -480,6 +524,25 @@ export interface IntrospectedRoute {
|
|
|
480
524
|
* implementation detail — userland code should never depend on it.
|
|
481
525
|
*/
|
|
482
526
|
export declare const DALOY_RAW_BODY: unique symbol;
|
|
527
|
+
/**
|
|
528
|
+
* Internal Symbol used by adapters to stash a pre-buffered request body on
|
|
529
|
+
* the `Request` instance. When set, {@link readBodyLimited} skips the
|
|
530
|
+
* `ReadableStream` reader loop and returns the cached bytes directly after
|
|
531
|
+
* re-checking them against the caller-supplied limit. Adapters MUST only
|
|
532
|
+
* attach bytes they have already validated against the configured
|
|
533
|
+
* {@link AppOptions.bodyLimitBytes}; the limit re-check in
|
|
534
|
+
* `readBodyLimited` is defense-in-depth, not the primary cap. Module-public
|
|
535
|
+
* so first-party adapters can opt in; not part of the userland API surface.
|
|
536
|
+
*/
|
|
537
|
+
export declare const DALOY_REQUEST_RAW_BODY: unique symbol;
|
|
538
|
+
/**
|
|
539
|
+
* Internal Symbol set by handlers/serializers to attach a raw stream
|
|
540
|
+
* (Node `Readable` or Web `ReadableStream`) to a `Response`. The Node
|
|
541
|
+
* adapter pipes the stream straight to the socket, skipping the
|
|
542
|
+
* Web-stream reader bridge. Module-public so first-party adapters can
|
|
543
|
+
* opt in; userland code should not depend on it.
|
|
544
|
+
*/
|
|
545
|
+
export declare const DALOY_RAW_STREAM: unique symbol;
|
|
483
546
|
/**
|
|
484
547
|
* Contract-first HTTP application.
|
|
485
548
|
*
|
|
@@ -535,6 +598,14 @@ export declare class App {
|
|
|
535
598
|
/** Public registry: enables OpenAPI gen, typed-client gen, dead-route detection. */
|
|
536
599
|
readonly routes: RouteDefinition<any, any, any, any>[];
|
|
537
600
|
private router;
|
|
601
|
+
/**
|
|
602
|
+
* Memoized result of `isProduction()`. The inputs (`options.env`,
|
|
603
|
+
* `options.production`, `process.env.NODE_ENV`) cannot change between
|
|
604
|
+
* the moment a route is dispatched and the moment its error is rendered,
|
|
605
|
+
* so reading `process.env.NODE_ENV` on every error response is wasted
|
|
606
|
+
* work in the hot path. Computed lazily on first read.
|
|
607
|
+
*/
|
|
608
|
+
private _productionCache;
|
|
538
609
|
/** WebSocket route registry. Adapters look up handlers via `app.webSocketRoutes.find()`. */
|
|
539
610
|
readonly webSocketRoutes: WebSocketRegistry;
|
|
540
611
|
private prefix;
|
|
@@ -545,6 +616,13 @@ export declare class App {
|
|
|
545
616
|
private routeSecurityMarkers;
|
|
546
617
|
/** Decorator bag merged into ctx.state on every request. */
|
|
547
618
|
private decorations;
|
|
619
|
+
/**
|
|
620
|
+
* Count of own keys on {@link decorations}. Tracked alongside the bag so the
|
|
621
|
+
* dispatch hot path can take a `count === 0` fast path and skip the
|
|
622
|
+
* `Object.assign` spread on the common case (no `app.decorate()` calls).
|
|
623
|
+
* Updated only when {@link decorate} mutates the bag.
|
|
624
|
+
*/
|
|
625
|
+
private decorationsCount;
|
|
548
626
|
private installedPlugins;
|
|
549
627
|
private closeHooks;
|
|
550
628
|
private closeHooksRun;
|
|
@@ -608,6 +686,41 @@ export declare class App {
|
|
|
608
686
|
* set deep in shared configuration.
|
|
609
687
|
*/
|
|
610
688
|
private assertInsecureDefaultsAcknowledged;
|
|
689
|
+
/**
|
|
690
|
+
* Emit the one-time boot audit entry for an applied security preset.
|
|
691
|
+
* Called from the constructor with the *original* (pre-preset) options
|
|
692
|
+
* so the log captures which fields the preset filled in vs. which the
|
|
693
|
+
* caller set explicitly. Logged at `info` so the line shows up in
|
|
694
|
+
* standard production log shipping without being noisy.
|
|
695
|
+
*
|
|
696
|
+
* Operators can audit the live posture at any time through
|
|
697
|
+
* {@link App.getSecurityPosture}.
|
|
698
|
+
*
|
|
699
|
+
* @since 0.34.0
|
|
700
|
+
*/
|
|
701
|
+
private logSecurityPresetIfApplied;
|
|
702
|
+
/**
|
|
703
|
+
* Structured snapshot of the live security posture. Returns the same
|
|
704
|
+
* data the constructor logs under the `security.preset.applied` audit
|
|
705
|
+
* event plus the resolved values of every secure-by-default knob, so
|
|
706
|
+
* operators can build a `/__security` introspection route or a CI
|
|
707
|
+
* audit without parsing the framework source.
|
|
708
|
+
*
|
|
709
|
+
* @since 0.34.0
|
|
710
|
+
*/
|
|
711
|
+
getSecurityPosture(): {
|
|
712
|
+
preset: SecurityPreset | undefined;
|
|
713
|
+
secureDefaults: boolean;
|
|
714
|
+
secureHeaders: boolean;
|
|
715
|
+
corsCrossOriginGuard: boolean;
|
|
716
|
+
csrf: "off" | "on";
|
|
717
|
+
crashOnUnhandledRejection: boolean | "default";
|
|
718
|
+
trustProxy: true | false | "unconfigured";
|
|
719
|
+
bodyLimitBytes: number;
|
|
720
|
+
requestTimeoutMs: number;
|
|
721
|
+
stripServerHeaders: boolean;
|
|
722
|
+
production: boolean;
|
|
723
|
+
};
|
|
611
724
|
/**
|
|
612
725
|
* Install the secure-by-default global hooks. Currently:
|
|
613
726
|
* - {@link secureHeaders} as a group-level hook so every response carries
|