@daloyjs/core 0.37.0 → 0.38.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/LICENSE +21 -0
- package/README.md +13 -1
- package/dist/adapters/node.d.ts +14 -0
- package/dist/adapters/node.js +8 -0
- package/dist/app.d.ts +56 -5
- package/dist/app.js +27 -49
- package/dist/cli.js +25 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +16 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +30 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +34 -0
- package/dist/security.js +48 -1
- package/package.json +4 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DaloyJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -9,11 +9,17 @@
|
|
|
9
9
|
|
|
10
10
|
# DaloyJS
|
|
11
11
|
|
|
12
|
+
[](LICENSE)
|
|
12
13
|
[](https://github.com/daloyjs/daloy/actions/workflows/ci.yml)
|
|
13
14
|
[](https://github.com/daloyjs/daloy/actions/workflows/codeql.yml)
|
|
14
15
|
[](https://github.com/daloyjs/daloy/actions/workflows/release.yml)
|
|
15
|
-
[](https://securityscorecards.dev/viewer/?uri=github.com/daloyjs/daloy)
|
|
16
16
|
[](https://github.com/daloyjs/daloy/actions/workflows/zizmor.yml)
|
|
17
|
+
[](https://github.com/daloyjs/daloy/commits/main)
|
|
18
|
+
[](https://jsr.io/@daloyjs/daloy)
|
|
19
|
+
[](https://www.bestpractices.dev/projects/13058)
|
|
20
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/daloyjs/daloy)
|
|
21
|
+
[](https://github.com/daloyjs/daloy/blob/main/SECURITY.md)
|
|
17
23
|
|
|
18
24
|
> A **runtime-portable TypeScript web framework** with built-in **contract-first routing**, **validation**, **OpenAPI (Hey API)**, **typed client generation**, **large-scale maintainability**, and **security-focused runtime plus supply-chain posture**.
|
|
19
25
|
|
|
@@ -221,6 +227,10 @@ const r = await client.getBookById({ params: { id: "1" } });
|
|
|
221
227
|
// ^? { status: 200; body: { id: string; title: string } } | { status: 404; ... }
|
|
222
228
|
```
|
|
223
229
|
|
|
230
|
+
> Method inference relies on **chaining** your `app.route(...)` calls (`new App().route(a).route(b)`) and letting
|
|
231
|
+
> TypeScript infer the variable's type. A widening `const app: App` annotation, a `: App` factory return type, or
|
|
232
|
+
> registering routes as separate statements erases the per-route types and collapses the client to an untyped surface.
|
|
233
|
+
|
|
224
234
|
---
|
|
225
235
|
|
|
226
236
|
## Built-in docs UI (Scalar / Swagger UI)
|
|
@@ -311,6 +321,7 @@ deployment.
|
|
|
311
321
|
| **Header / response splitting** | Core header sanitizers reject CRLF + NUL. |
|
|
312
322
|
| **Path traversal** | Core router rejects `..` segments and `//` before walking. |
|
|
313
323
|
| **Slow-loris / hung handlers** | Core `requestTimeoutMs` aborts handlers (default 30 s); Node adapter sets `requestTimeout` + `headersTimeout` + `maxHeaderSize`. |
|
|
324
|
+
| **HTTP/2 Bomb / header-count flood** | Core `maxHeaderCount` rejects requests with more than 100 header fields (`431`) before routing; Node adapter sets `server.maxHeadersCount`. See [SECURITY.md](SECURITY.md) for the upstream HTTP/2 mitigations. |
|
|
314
325
|
| **MIME sniffing** | First-party `secureHeaders()` sets `X-Content-Type-Options: nosniff`; scaffolded apps enable it. |
|
|
315
326
|
| **Clickjacking** | First-party `secureHeaders()` sets `X-Frame-Options: DENY` + CSP `frame-ancestors 'none'`; scaffolded apps enable it. |
|
|
316
327
|
| **XSS via injected scripts** | First-party `secureHeaders()` provides a strict CSP `default-src 'self'` baseline; the directives-object form supports per-request **nonces** and **Trusted Types** (`require-trusted-types-for 'script'`). |
|
|
@@ -556,6 +567,7 @@ A growing suite of static gates runs on every push and PR:
|
|
|
556
567
|
- Parity / governance / runtime-parity / routing-hardening audits: `verify:parity-audits`, `verify:governance-audits`, `verify:runtime-parity-audits`, `verify:routing-hardening-audits`.
|
|
557
568
|
- Source-tree gates: `verify:no-shrinkwrap`, `verify:no-bin-shadowing`, `verify:no-native-addons`, `verify:no-polyfill-cdns` (hijacked-CDN IOCs and typosquats), `verify:no-redos-patterns`, `verify:no-encoded-payloads`, `verify:no-invisible-unicode`, `verify:no-weak-random`, `verify:no-unsafe-buffer`, `verify:no-leaked-credentials`, `verify:no-vulnerable-sandboxes`.
|
|
558
569
|
- Agent-skill gates: `verify:no-leaky-agent-skills`, `verify:no-toxic-agent-skills`, `verify:no-toxic-skills` — scanning every agent-instruction surface (`SKILL.md`, `AGENTS.md`, `copilot-instructions.md`, `.cursorrules`, `CLAUDE.md`, `*.instructions.md`, `*.prompt.md`); the `.cursorrules` / `CLAUDE.md` filenames cover the **TrapDoor** crypto-stealer's AI-agent-config prompt-injection persistence ([Socket, 2026-05-24](https://socket.dev/blog/trapdoor-crypto-stealer)).
|
|
570
|
+
- Agent / editor config-autorun gate: `verify:no-agent-config-autorun` — refuses editor / AI-coding-agent config files that auto-execute a command on folder open or session start (VS Code `folderOpen` task, Claude/Gemini `"type": "command"` hook, Cursor `alwaysApply` run-a-script rule, a `package.json` `"test": "node .github/setup.js"` hijack, or a loose `.github/` dropper), covering the **Miasma** worm's config-injection detonation surface ([SafeDep, 2026-06-05](https://safedep.io/miasma-worm-ai-coding-agent-config-injection/)).
|
|
559
571
|
- Dependency gates: `verify:no-runtime-deps`, `verify:dep-licenses`, `verify:known-dep-names`, `verify:lockfile-sources`, `verify:no-registry-exfiltration`, `verify:no-remote-exec`, `verify:no-lifecycle-scripts`, `verify:runtime-eol` (refuses to release on a Node line past its EOL date).
|
|
560
572
|
- IOC coverage in `verify:no-registry-exfiltration` and `verify:lockfile-sources` for active campaigns including Beamglea phishing-CDN, `naya-flore` / `nvlore-hsc` WhatsApp remote-kill-switch, the Toptal GitHub-org hijack, `xuxingfeng` and `xlsx-to-json-lh` destructive payloads, `react-login-page` keylogger, `@crypto-exploit` wallet drainers, Vietnam-Telegram-ban Fastlane typosquats, surveillance-malware packages, the Discord-webhook reconnaissance campaign, the `codexui-android` AI-coding-agent token theft (reads of `~/.codex/auth.json` / `~/.claude/`), and npm-package-aliasing dependency-confusion patterns.
|
|
561
573
|
- `SECURITY-CONTACTS.md` rotation file with a machine-readable ACTIVE block and `<!-- last-exercise: -->` marker; the release workflow refuses to publish when `github.actor` is not on the ACTIVE rotation.
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -16,6 +16,20 @@ export interface NodeServerOptions {
|
|
|
16
16
|
handleSignals?: boolean;
|
|
17
17
|
/** Maximum HTTP header size bytes (DoS protection). Default: 16 KiB. */
|
|
18
18
|
maxHeaderBytes?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of incoming HTTP header fields, forwarded to Node's
|
|
21
|
+
* `server.maxHeadersCount`. This is the native, parser-level counterpart to
|
|
22
|
+
* the framework's portable {@link "../app.js".AppOptions.maxHeaderCount}
|
|
23
|
+
* guard: a header-count flood is dropped by the HTTP parser before it ever
|
|
24
|
+
* becomes a `Request`, which is the cheapest place to shed header-count
|
|
25
|
+
* amplification (the dimension abused by the "HTTP/2 Bomb"). Node's own
|
|
26
|
+
* default is `2000`; this adapter tightens it to `100` to mirror the
|
|
27
|
+
* application-tier cap. Set `0` to disable (use Node's unbounded default).
|
|
28
|
+
* Default: 100.
|
|
29
|
+
*
|
|
30
|
+
* @since 0.38.0
|
|
31
|
+
*/
|
|
32
|
+
maxHeaderCount?: number;
|
|
19
33
|
/**
|
|
20
34
|
* Maximum number of concurrent sockets the server will accept, forwarded to
|
|
21
35
|
* Node's `server.maxConnections`. Acts as connection-layer admission
|
package/dist/adapters/node.js
CHANGED
|
@@ -36,6 +36,14 @@ export function serve(app, opts = {}) {
|
|
|
36
36
|
server.requestTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
37
37
|
server.headersTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
38
38
|
server.keepAliveTimeout = 5_000;
|
|
39
|
+
// Native parser-level header-count cap. Drops header-count floods (the
|
|
40
|
+
// "HTTP/2 Bomb" amplification dimension) before they become a Request.
|
|
41
|
+
// `0` opts out and restores Node's unbounded-ish default (2000).
|
|
42
|
+
const maxHeaderCount = opts.maxHeaderCount;
|
|
43
|
+
server.maxHeadersCount =
|
|
44
|
+
typeof maxHeaderCount === "number" && maxHeaderCount >= 0
|
|
45
|
+
? maxHeaderCount
|
|
46
|
+
: 100;
|
|
39
47
|
// Connection-layer admission control. Reject overflow sockets at accept time
|
|
40
48
|
// rather than queuing them into the event loop under overload.
|
|
41
49
|
if (typeof opts.maxConnections === "number" && opts.maxConnections > 0) {
|
package/dist/app.d.ts
CHANGED
|
@@ -75,6 +75,19 @@ export interface AppOptions {
|
|
|
75
75
|
allowedContentTypes?: string[];
|
|
76
76
|
/** Per-request timeout in ms (handler + hooks). Default: 30000. Set 0 to disable. */
|
|
77
77
|
requestTimeoutMs?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of distinct request header fields accepted before the
|
|
80
|
+
* request is rejected with `431 Request Header Fields Too Large`. This is
|
|
81
|
+
* the runtime-portable, application-tier defence against header-*count*
|
|
82
|
+
* amplification (the dimension abused by the "HTTP/2 Bomb", where
|
|
83
|
+
* per-header server-side bookkeeping — not header size — is the
|
|
84
|
+
* amplifier). It complements the native header-count caps a runtime/proxy
|
|
85
|
+
* terminating HTTP/2 must apply (NGINX `max_headers`, Node
|
|
86
|
+
* `server.maxHeadersCount`). Set `0` to disable. Default: 100.
|
|
87
|
+
*
|
|
88
|
+
* @since 0.38.0
|
|
89
|
+
*/
|
|
90
|
+
maxHeaderCount?: number;
|
|
78
91
|
/**
|
|
79
92
|
* Per-request limits applied when parsing `multipart/form-data` bodies.
|
|
80
93
|
* These run in addition to `bodyLimitBytes`. Use them to cap the size of
|
|
@@ -663,13 +676,37 @@ export declare const DALOY_RAW_STREAM: unique symbol;
|
|
|
663
676
|
* serve(app, { port: 3000 });
|
|
664
677
|
* ```
|
|
665
678
|
*
|
|
679
|
+
/**
|
|
680
|
+
* Append a freshly-registered route to an `App`'s accumulated route tuple.
|
|
681
|
+
*
|
|
682
|
+
* A newly-constructed `App` starts with the permissive default
|
|
683
|
+
* `readonly RouteDefinition<any, any, any, any>[]` so that bare `App`
|
|
684
|
+
* annotations (e.g. `serve(app: App)`) accept any instance. The first
|
|
685
|
+
* {@link App.route} call resets that wide default to a clean single-element
|
|
686
|
+
* tuple, so the typed client (`createClient(app)`) is keyed only by the
|
|
687
|
+
* routes the caller actually registered; subsequent calls append precisely.
|
|
688
|
+
*
|
|
689
|
+
* @typeParam Routes - The current accumulated route tuple.
|
|
690
|
+
* @typeParam R - The route definition being registered.
|
|
691
|
+
*/
|
|
692
|
+
type AppendRoute<Routes extends readonly RouteDefinition<any, any, any, any>[], R extends RouteDefinition<any, any, any, any>> = readonly RouteDefinition<any, any, any, any>[] extends Routes ? readonly [R] : readonly [...Routes, R];
|
|
693
|
+
/**
|
|
666
694
|
* @since 0.1.0
|
|
667
695
|
*/
|
|
668
|
-
export declare class App {
|
|
696
|
+
export declare class App<Routes extends readonly RouteDefinition<any, any, any, any>[] = readonly RouteDefinition<any, any, any, any>[]> {
|
|
669
697
|
readonly options: Required<Pick<AppOptions, "validateResponses" | "bodyLimitBytes" | "requestTimeoutMs">> & AppOptions;
|
|
670
698
|
readonly log: Logger;
|
|
671
|
-
/**
|
|
672
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Public registry: enables OpenAPI gen, typed-client gen, dead-route detection.
|
|
701
|
+
*
|
|
702
|
+
* Statically the property is typed as the `Routes` tuple so that
|
|
703
|
+
* {@link App.route} can accumulate each registered route's literal
|
|
704
|
+
* `operationId`, request, and response types. The typed client
|
|
705
|
+
* (`createClient(app)`) reads this tuple to derive a precisely-typed method
|
|
706
|
+
* per route. At runtime it is an ordinary growable array — the tuple typing
|
|
707
|
+
* is a compile-time view only.
|
|
708
|
+
*/
|
|
709
|
+
readonly routes: Routes;
|
|
673
710
|
private router;
|
|
674
711
|
/**
|
|
675
712
|
* Memoized result of `isProduction()`. The inputs (`options.env`,
|
|
@@ -797,6 +834,7 @@ export declare class App {
|
|
|
797
834
|
trustProxy: true | false | "unconfigured";
|
|
798
835
|
bodyLimitBytes: number;
|
|
799
836
|
requestTimeoutMs: number;
|
|
837
|
+
maxHeaderCount: number;
|
|
800
838
|
stripServerHeaders: boolean;
|
|
801
839
|
production: boolean;
|
|
802
840
|
};
|
|
@@ -921,10 +959,22 @@ export declare class App {
|
|
|
921
959
|
* });
|
|
922
960
|
* ```
|
|
923
961
|
*
|
|
962
|
+
* The return type widens `Routes` with the freshly-registered route so
|
|
963
|
+
* that chained registration (`new App().route(a).route(b)`) accumulates a
|
|
964
|
+
* precise tuple. The typed client (`createClient(app)`) consumes that tuple
|
|
965
|
+
* to expose a method per `operationId` with parameters and responses
|
|
966
|
+
* inferred from the route's own schemas. Non-chained calls
|
|
967
|
+
* (`app.route(a); app.route(b);`) keep the variable's original type, so
|
|
968
|
+
* chain the calls when you want the inferred client surface.
|
|
969
|
+
*
|
|
924
970
|
* @param def - The route definition.
|
|
925
|
-
* @returns This `App` instance for chaining.
|
|
971
|
+
* @returns This `App` instance (widened with the new route) for chaining.
|
|
926
972
|
*/
|
|
927
|
-
route<P extends PathString, M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap>(def: RouteDefinition<P, M, Req, Res>
|
|
973
|
+
route<P extends PathString, M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(def: RouteDefinition<P, M, Req, Res> & {
|
|
974
|
+
operationId?: Op;
|
|
975
|
+
}): App<AppendRoute<Routes, RouteDefinition<P, M, Req, Res> & {
|
|
976
|
+
operationId: Op;
|
|
977
|
+
}>>;
|
|
928
978
|
/**
|
|
929
979
|
* Register a WebSocket route. The handler runs when an HTTP client sends an
|
|
930
980
|
* `Upgrade: websocket` request to `path`; the adapter performs the RFC 6455
|
|
@@ -1332,3 +1382,4 @@ export declare function createApp(options?: AppOptions): App;
|
|
|
1332
1382
|
* @internal
|
|
1333
1383
|
*/
|
|
1334
1384
|
export declare function _resetPackageJsonCacheForTests(): void;
|
|
1385
|
+
export {};
|
package/dist/app.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Router } from "./router.js";
|
|
|
2
2
|
import { WebSocketRegistry, normalizeWebSocketOptions, } from "./websocket.js";
|
|
3
3
|
import { BadRequestError, ForbiddenError, HttpError, InternalError, MethodNotAllowedError, NotFoundError, PayloadTooLargeError, RequestTimeoutError, TooManyRequestsError, UnsupportedMediaTypeError, ValidationError, } from "./errors.js";
|
|
4
4
|
import { validate } from "./schema.js";
|
|
5
|
-
import { readBodyLimited, safeJsonParse, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertStrongSecret, timingSafeEqual, isForbiddenObjectKey } from "./security.js";
|
|
5
|
+
import { readBodyLimited, safeJsonParse, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, assertStrongSecret, timingSafeEqual, isForbiddenObjectKey } from "./security.js";
|
|
6
6
|
import { createLogger, noopLogger } from "./logger.js";
|
|
7
7
|
import { generateOpenAPI, openapiToYAML, } from "./openapi.js";
|
|
8
8
|
import { docsContentSecurityPolicy, scalarHtml, swaggerUiHtml, } from "./docs.js";
|
|
@@ -149,6 +149,7 @@ function applySecurityPreset(options) {
|
|
|
149
149
|
const DEFAULTS = {
|
|
150
150
|
bodyLimitBytes: 1024 * 1024,
|
|
151
151
|
requestTimeoutMs: 30_000,
|
|
152
|
+
maxHeaderCount: DEFAULT_MAX_HEADER_COUNT,
|
|
152
153
|
validateResponses: true,
|
|
153
154
|
};
|
|
154
155
|
const TEXT_ENCODER = new TextEncoder();
|
|
@@ -181,58 +182,21 @@ export const DALOY_REQUEST_RAW_BODY = Symbol.for("daloyjs.request.rawBody");
|
|
|
181
182
|
*/
|
|
182
183
|
export const DALOY_RAW_STREAM = Symbol.for("daloyjs.response.rawStream");
|
|
183
184
|
/**
|
|
184
|
-
* Contract-first HTTP application.
|
|
185
|
-
*
|
|
186
|
-
* `App` is the top-level entry point: register {@link RouteDefinition routes}
|
|
187
|
-
* with {@link App.route}, layer cross-cutting behavior with
|
|
188
|
-
* {@link App.use}/{@link App.register}, then expose the application to a
|
|
189
|
-
* runtime via {@link App.fetch} (Web standard) or one of the adapter subpaths
|
|
190
|
-
* such as `@daloyjs/core/node`, `@daloyjs/core/cloudflare`, or
|
|
191
|
-
* `@daloyjs/core/lambda`.
|
|
192
|
-
*
|
|
193
|
-
* The same `App` instance powers:
|
|
194
|
-
*
|
|
195
|
-
* - request routing (`Router` under the hood)
|
|
196
|
-
* - request/response validation against Standard-Schema validators
|
|
197
|
-
* - OpenAPI 3.1 generation (`generateOpenAPI(app)`)
|
|
198
|
-
* - typed in-process client (`createClient(app)`) and generated SDK
|
|
199
|
-
* - graceful shutdown and lifecycle observability
|
|
200
|
-
*
|
|
201
|
-
* `App` is **runtime-agnostic**: the same instance runs on Node, Bun, Deno,
|
|
202
|
-
* Cloudflare Workers, Vercel Edge, AWS Lambda, and Fastly Compute via the
|
|
203
|
-
* dedicated adapters.
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```ts
|
|
207
|
-
* import { App, secureHeaders } from "@daloyjs/core";
|
|
208
|
-
* import { z } from "zod";
|
|
209
|
-
*
|
|
210
|
-
* const app = new App({ title: "Books API", version: "1.0.0" });
|
|
211
|
-
*
|
|
212
|
-
* app.use(secureHeaders());
|
|
213
|
-
*
|
|
214
|
-
* app.route({
|
|
215
|
-
* method: "GET",
|
|
216
|
-
* path: "/books/:id",
|
|
217
|
-
* operationId: "getBook",
|
|
218
|
-
* request: { params: z.object({ id: z.uuid() }) },
|
|
219
|
-
* responses: {
|
|
220
|
-
* 200: { description: "OK", body: z.object({ id: z.string(), title: z.string() }) },
|
|
221
|
-
* },
|
|
222
|
-
* handler: ({ params }) => ({ status: 200, body: { id: params.id, title: "Dune" } }),
|
|
223
|
-
* });
|
|
224
|
-
*
|
|
225
|
-
* // Node:
|
|
226
|
-
* import { serve } from "@daloyjs/core/node";
|
|
227
|
-
* serve(app, { port: 3000 });
|
|
228
|
-
* ```
|
|
229
|
-
*
|
|
230
185
|
* @since 0.1.0
|
|
231
186
|
*/
|
|
232
187
|
export class App {
|
|
233
188
|
options;
|
|
234
189
|
log;
|
|
235
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Public registry: enables OpenAPI gen, typed-client gen, dead-route detection.
|
|
192
|
+
*
|
|
193
|
+
* Statically the property is typed as the `Routes` tuple so that
|
|
194
|
+
* {@link App.route} can accumulate each registered route's literal
|
|
195
|
+
* `operationId`, request, and response types. The typed client
|
|
196
|
+
* (`createClient(app)`) reads this tuple to derive a precisely-typed method
|
|
197
|
+
* per route. At runtime it is an ordinary growable array — the tuple typing
|
|
198
|
+
* is a compile-time view only.
|
|
199
|
+
*/
|
|
236
200
|
routes = [];
|
|
237
201
|
router = new Router();
|
|
238
202
|
/**
|
|
@@ -328,6 +292,7 @@ export class App {
|
|
|
328
292
|
validateResponses: resolved.validateResponses ?? DEFAULTS.validateResponses,
|
|
329
293
|
bodyLimitBytes: resolved.bodyLimitBytes ?? DEFAULTS.bodyLimitBytes,
|
|
330
294
|
requestTimeoutMs: resolved.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs,
|
|
295
|
+
maxHeaderCount: resolved.maxHeaderCount ?? DEFAULTS.maxHeaderCount,
|
|
331
296
|
...resolved,
|
|
332
297
|
};
|
|
333
298
|
this.log =
|
|
@@ -456,6 +421,7 @@ export class App {
|
|
|
456
421
|
trustProxy: o.trustProxy === undefined ? "unconfigured" : o.trustProxy,
|
|
457
422
|
bodyLimitBytes: this.options.bodyLimitBytes,
|
|
458
423
|
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
424
|
+
maxHeaderCount: this.options.maxHeaderCount ?? DEFAULT_MAX_HEADER_COUNT,
|
|
459
425
|
stripServerHeaders: o.stripServerHeaders !== false,
|
|
460
426
|
production: this.isProduction(),
|
|
461
427
|
});
|
|
@@ -915,8 +881,16 @@ export class App {
|
|
|
915
881
|
* });
|
|
916
882
|
* ```
|
|
917
883
|
*
|
|
884
|
+
* The return type widens `Routes` with the freshly-registered route so
|
|
885
|
+
* that chained registration (`new App().route(a).route(b)`) accumulates a
|
|
886
|
+
* precise tuple. The typed client (`createClient(app)`) consumes that tuple
|
|
887
|
+
* to expose a method per `operationId` with parameters and responses
|
|
888
|
+
* inferred from the route's own schemas. Non-chained calls
|
|
889
|
+
* (`app.route(a); app.route(b);`) keep the variable's original type, so
|
|
890
|
+
* chain the calls when you want the inferred client surface.
|
|
891
|
+
*
|
|
918
892
|
* @param def - The route definition.
|
|
919
|
-
* @returns This `App` instance for chaining.
|
|
893
|
+
* @returns This `App` instance (widened with the new route) for chaining.
|
|
920
894
|
*/
|
|
921
895
|
route(def) {
|
|
922
896
|
// Refuse non-canonical HTTP methods at runtime.
|
|
@@ -965,6 +939,9 @@ export class App {
|
|
|
965
939
|
...sources,
|
|
966
940
|
]);
|
|
967
941
|
this.router.add(def.method, fullPath, { def: merged, hooks, mergedHooks, hasFinalizeHook, corsOriginAllows, fullCorsOriginAllows }, def.operationId);
|
|
942
|
+
// `routes` is statically a readonly tuple so the typed client can infer
|
|
943
|
+
// per-route methods; at runtime it is a growable array, so we push through
|
|
944
|
+
// a mutable view.
|
|
968
945
|
this.routes.push(merged);
|
|
969
946
|
this.routeSecurityMarkers.push({
|
|
970
947
|
method: merged.method,
|
|
@@ -1834,6 +1811,7 @@ export class App {
|
|
|
1834
1811
|
try {
|
|
1835
1812
|
assertNoDuplicateSingletonHeaders(request.headers);
|
|
1836
1813
|
assertNoReservedInternalHeaders(request.headers);
|
|
1814
|
+
assertHeaderCountWithinLimit(request.headers, this.options.maxHeaderCount ?? DEFAULT_MAX_HEADER_COUNT);
|
|
1837
1815
|
this.assertTrustProxyConfigured(request);
|
|
1838
1816
|
this.assertBootGuards();
|
|
1839
1817
|
if (globalHooks.onRequest !== undefined) {
|
package/dist/cli.js
CHANGED
|
@@ -651,6 +651,31 @@ async function runDoctor(opts, io) {
|
|
|
651
651
|
"JSON parsers are not DoS-amplified by a multipart-sized blob.",
|
|
652
652
|
});
|
|
653
653
|
}
|
|
654
|
+
// Header-count cap audit. The framework's portable maxHeaderCount
|
|
655
|
+
// guard is the application-tier defence against header-*count*
|
|
656
|
+
// amplification (the "HTTP/2 Bomb" dimension). Surface a finding when
|
|
657
|
+
// it is disabled (0) or raised to an implausibly generous value, both
|
|
658
|
+
// of which let a header flood reach routing.
|
|
659
|
+
const maxHeaderCount = o.maxHeaderCount;
|
|
660
|
+
if (maxHeaderCount === 0) {
|
|
661
|
+
findings.push({
|
|
662
|
+
level: "warn",
|
|
663
|
+
code: "audit.maxHeaderCount.disabled",
|
|
664
|
+
message: "maxHeaderCount is 0 — the header-count flood guard is disabled. " +
|
|
665
|
+
"A request carrying thousands of header fields reaches routing. " +
|
|
666
|
+
"Keep a finite cap (default 100) unless an upstream proxy already " +
|
|
667
|
+
"enforces one (NGINX max_headers, Node server.maxHeadersCount).",
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else if (typeof maxHeaderCount === "number" && maxHeaderCount > 1000) {
|
|
671
|
+
findings.push({
|
|
672
|
+
level: "warn",
|
|
673
|
+
code: "audit.maxHeaderCount.blanket",
|
|
674
|
+
message: `maxHeaderCount is ${maxHeaderCount} (> 1000). Realistic requests ` +
|
|
675
|
+
"carry a few dozen headers; a cap this high weakens the " +
|
|
676
|
+
"header-count amplification defence.",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
654
679
|
// Idle-timeout / request-timeout audit. Reaffirms the
|
|
655
680
|
// existing requestTimeoutMs check; also surface an explicit zero
|
|
656
681
|
// idleTimeoutMs in production. The framework also keeps adapter
|
package/dist/client.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export type RoutesOf<A extends App> = A["routes"][number];
|
|
|
17
17
|
* Typed client surface generated from an `App`. The result is a record keyed
|
|
18
18
|
* by each route's `operationId` whose values are async methods inferred from
|
|
19
19
|
* the route's request and response schemas.
|
|
20
|
+
*
|
|
21
|
+
* The per-method types are recovered from the `App`'s accumulated route tuple,
|
|
22
|
+
* which is built up as you **chain** `app.route(...)` calls. If the `App` type
|
|
23
|
+
* is widened back to its bare default — e.g. a `const app: App` annotation, a
|
|
24
|
+
* `: App` factory return type, or registering routes as separate statements
|
|
25
|
+
* rather than a chain — the tuple is erased and this type collapses to an
|
|
26
|
+
* untyped, string-indexed record.
|
|
20
27
|
*/
|
|
21
28
|
export type ClientFor<A extends App> = {
|
|
22
29
|
[R in Extract<RoutesOf<A>, {
|
|
@@ -58,10 +65,26 @@ export interface ClientOptions {
|
|
|
58
65
|
* For non-TypeScript consumers, run `pnpm gen` to emit a fully-typed SDK
|
|
59
66
|
* from the OpenAPI document instead.
|
|
60
67
|
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple,
|
|
70
|
+
* so chain your `app.route(...)` registrations and let TypeScript infer the
|
|
71
|
+
* variable's type. A widening `const app: App` annotation, a `: App` factory
|
|
72
|
+
* return type, or registering routes as separate statements erases the
|
|
73
|
+
* per-route types and yields an untyped client.
|
|
74
|
+
*
|
|
61
75
|
* @example
|
|
62
76
|
* ```ts
|
|
63
77
|
* import { createClient } from "@daloyjs/core/client";
|
|
64
78
|
*
|
|
79
|
+
* const app = new App().route({
|
|
80
|
+
* method: "GET",
|
|
81
|
+
* path: "/books/:id",
|
|
82
|
+
* operationId: "getBook",
|
|
83
|
+
* request: { params: z.object({ id: z.string() }) },
|
|
84
|
+
* responses: { 200: { description: "OK", body: z.object({ id: z.string(), title: z.string() }) } },
|
|
85
|
+
* handler: ({ params }) => ({ status: 200, body: { id: params.id, title: "Dune" } }),
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
65
88
|
* const client = createClient(app, { baseUrl: "https://api.example.com" });
|
|
66
89
|
* const res = await client.getBook({ params: { id: "123" } });
|
|
67
90
|
* if (res.status === 200) console.log(res.body.title);
|
package/dist/client.js
CHANGED
|
@@ -22,10 +22,26 @@
|
|
|
22
22
|
* For non-TypeScript consumers, run `pnpm gen` to emit a fully-typed SDK
|
|
23
23
|
* from the OpenAPI document instead.
|
|
24
24
|
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple,
|
|
27
|
+
* so chain your `app.route(...)` registrations and let TypeScript infer the
|
|
28
|
+
* variable's type. A widening `const app: App` annotation, a `: App` factory
|
|
29
|
+
* return type, or registering routes as separate statements erases the
|
|
30
|
+
* per-route types and yields an untyped client.
|
|
31
|
+
*
|
|
25
32
|
* @example
|
|
26
33
|
* ```ts
|
|
27
34
|
* import { createClient } from "@daloyjs/core/client";
|
|
28
35
|
*
|
|
36
|
+
* const app = new App().route({
|
|
37
|
+
* method: "GET",
|
|
38
|
+
* path: "/books/:id",
|
|
39
|
+
* operationId: "getBook",
|
|
40
|
+
* request: { params: z.object({ id: z.string() }) },
|
|
41
|
+
* responses: { 200: { description: "OK", body: z.object({ id: z.string(), title: z.string() }) } },
|
|
42
|
+
* handler: ({ params }) => ({ status: 200, body: { id: params.id, title: "Dune" } }),
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
29
45
|
* const client = createClient(app, { baseUrl: "https://api.example.com" });
|
|
30
46
|
* const res = await client.getBook({ params: { id: "123" } });
|
|
31
47
|
* if (res.status === 200) console.log(res.body.title);
|
package/dist/errors.d.ts
CHANGED
|
@@ -299,6 +299,29 @@ export declare class MethodNotAllowedError extends HttpError {
|
|
|
299
299
|
export declare class PayloadTooLargeError extends HttpError {
|
|
300
300
|
constructor(limit: number);
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* `431 Request Header Fields Too Large` — thrown when an incoming request
|
|
304
|
+
* carries more distinct header fields than {@link AppOptions.maxHeaderCount}.
|
|
305
|
+
*
|
|
306
|
+
* This is the portable, JS-layer counterpart to the native "max header
|
|
307
|
+
* count" caps that web servers and proxies expose (e.g. NGINX's
|
|
308
|
+
* `max_headers`, Node's `server.maxHeadersCount`). It is defence-in-depth
|
|
309
|
+
* against header-*count* amplification — the dimension abused by the
|
|
310
|
+
* "HTTP/2 Bomb" (Calif, 2026), where per-header server-side bookkeeping is
|
|
311
|
+
* the amplifier rather than header size. A flood of nearly-empty headers
|
|
312
|
+
* never reaches the router because the count cap fires first.
|
|
313
|
+
*
|
|
314
|
+
* The native HPACK session-memory pinning that the bomb relies on must
|
|
315
|
+
* still be mitigated at the runtime/proxy that terminates HTTP/2 (apply the
|
|
316
|
+
* vendor fix and cap the header count there); this guard protects the
|
|
317
|
+
* application tier on every runtime regardless of upstream configuration.
|
|
318
|
+
*
|
|
319
|
+
* @param limit - The configured maximum header count that was exceeded.
|
|
320
|
+
* @since 0.38.0
|
|
321
|
+
*/
|
|
322
|
+
export declare class RequestHeaderFieldsTooLargeError extends HttpError {
|
|
323
|
+
constructor(limit: number);
|
|
324
|
+
}
|
|
302
325
|
/**
|
|
303
326
|
* `415 Unsupported Media Type` — thrown when the request `Content-Type` is
|
|
304
327
|
* not in {@link AppOptions.allowedContentTypes} for a route that declares a
|
package/dist/errors.js
CHANGED
|
@@ -395,6 +395,36 @@ export class PayloadTooLargeError extends HttpError {
|
|
|
395
395
|
this.name = "PayloadTooLargeError";
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* `431 Request Header Fields Too Large` — thrown when an incoming request
|
|
400
|
+
* carries more distinct header fields than {@link AppOptions.maxHeaderCount}.
|
|
401
|
+
*
|
|
402
|
+
* This is the portable, JS-layer counterpart to the native "max header
|
|
403
|
+
* count" caps that web servers and proxies expose (e.g. NGINX's
|
|
404
|
+
* `max_headers`, Node's `server.maxHeadersCount`). It is defence-in-depth
|
|
405
|
+
* against header-*count* amplification — the dimension abused by the
|
|
406
|
+
* "HTTP/2 Bomb" (Calif, 2026), where per-header server-side bookkeeping is
|
|
407
|
+
* the amplifier rather than header size. A flood of nearly-empty headers
|
|
408
|
+
* never reaches the router because the count cap fires first.
|
|
409
|
+
*
|
|
410
|
+
* The native HPACK session-memory pinning that the bomb relies on must
|
|
411
|
+
* still be mitigated at the runtime/proxy that terminates HTTP/2 (apply the
|
|
412
|
+
* vendor fix and cap the header count there); this guard protects the
|
|
413
|
+
* application tier on every runtime regardless of upstream configuration.
|
|
414
|
+
*
|
|
415
|
+
* @param limit - The configured maximum header count that was exceeded.
|
|
416
|
+
* @since 0.38.0
|
|
417
|
+
*/
|
|
418
|
+
export class RequestHeaderFieldsTooLargeError extends HttpError {
|
|
419
|
+
constructor(limit) {
|
|
420
|
+
super(431, {
|
|
421
|
+
type: "https://daloyjs.dev/errors/request-header-fields-too-large",
|
|
422
|
+
title: "Request Header Fields Too Large",
|
|
423
|
+
detail: `Request carries more than ${limit} header fields`,
|
|
424
|
+
});
|
|
425
|
+
this.name = "RequestHeaderFieldsTooLargeError";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
398
428
|
/**
|
|
399
429
|
* `415 Unsupported Media Type` — thrown when the request `Content-Type` is
|
|
400
430
|
* not in {@link AppOptions.allowedContentTypes} for a route that declares a
|
package/dist/index.d.ts
CHANGED
|
@@ -11,13 +11,13 @@ export type { SubdomainsOptions, SubdomainsResult } from "./subdomains.js";
|
|
|
11
11
|
export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
12
12
|
export type { DependencyHooks, DependencyOptions, } from "./dependency.js";
|
|
13
13
|
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
14
|
-
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
14
|
+
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
15
15
|
export type { ProblemDetails, ProblemRenderOptions, HttpErrorOptions } from "./errors.js";
|
|
16
16
|
export type { StandardSchemaV1 } from "./schema.js";
|
|
17
17
|
export { validate, isStandardSchema } from "./schema.js";
|
|
18
18
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
19
19
|
export type { ChangeSeverity, OpenAPIChange, OpenAPIDiffResult, } from "./openapi-diff.js";
|
|
20
|
-
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
20
|
+
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
21
21
|
export type { WebhookHmacAlgorithm } from "./security.js";
|
|
22
22
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
23
23
|
export { etag } from "./etag.js";
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ export { _resetInsecureDefaultsLogForTests } from "./app.js";
|
|
|
6
6
|
export { getConnInfo, setConnInfo, assertBehindProxy, resolveClientIp, readRemoteAddress, readRemotePort, pickForwardedForByHops, } from "./conn-info.js";
|
|
7
7
|
export { subdomains, PSL_SNAPSHOT_DATE, PSL_PUBLIC_SUFFIXES, MAX_SNAPSHOT_AGE_DAYS, } from "./subdomains.js";
|
|
8
8
|
export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
9
|
-
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
9
|
+
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
10
10
|
export { validate, isStandardSchema } from "./schema.js";
|
|
11
11
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
12
|
-
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
12
|
+
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
13
13
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
14
14
|
export { etag } from "./etag.js";
|
|
15
15
|
export { compression, COMPRESSION_HOOK_MARKER, _resetCompressionRuntimeProbeForTests, } from "./compression.js";
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:c230d67e-08ae-5162-959c-66cbbb24366a",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-
|
|
7
|
+
"timestamp": "2026-06-10T11:58:42.238Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.38.0"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@0.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@0.38.0",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "0.
|
|
24
|
+
"version": "0.38.0",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@0.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@0.38.0",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-0.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-0.38.0",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "0.
|
|
51
|
+
"version": "0.38.0",
|
|
52
52
|
"tagVersion": 0,
|
|
53
53
|
"patch": false
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"components": [],
|
|
58
58
|
"dependencies": [
|
|
59
59
|
{
|
|
60
|
-
"ref": "pkg:npm/@daloyjs/core@0.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@0.38.0",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-0.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.
|
|
5
|
+
"name": "@daloyjs/core-0.38.0",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.38.0-c230d67e-08ae-5162-959c-66cbbb24366a",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-
|
|
8
|
+
"created": "2026-06-10T11:58:42.238Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "0.
|
|
19
|
+
"versionInfo": "0.38.0",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@0.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@0.38.0"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/security.d.ts
CHANGED
|
@@ -166,6 +166,40 @@ export declare const RESERVED_INBOUND_HEADER_PREFIXES: readonly string[];
|
|
|
166
166
|
* @since 0.36.0
|
|
167
167
|
*/
|
|
168
168
|
export declare function assertNoReservedInternalHeaders(headers: Headers): void;
|
|
169
|
+
/**
|
|
170
|
+
* Default cap on the number of distinct request header fields accepted
|
|
171
|
+
* before {@link assertHeaderCountWithinLimit} rejects the request. Chosen to
|
|
172
|
+
* sit far above any realistic legitimate request (browsers, tracing layers,
|
|
173
|
+
* and reverse proxies rarely add more than a few dozen headers) yet far
|
|
174
|
+
* below the thousands-of-headers floods used by header-count amplification
|
|
175
|
+
* attacks such as the "HTTP/2 Bomb".
|
|
176
|
+
*
|
|
177
|
+
* @since 0.38.0
|
|
178
|
+
*/
|
|
179
|
+
export declare const DEFAULT_MAX_HEADER_COUNT = 100;
|
|
180
|
+
/**
|
|
181
|
+
* Reject requests that carry more than `limit` distinct header fields.
|
|
182
|
+
*
|
|
183
|
+
* This is the runtime-portable, application-tier defence against
|
|
184
|
+
* header-*count* amplification (the dimension abused by the "HTTP/2 Bomb",
|
|
185
|
+
* where per-entry server bookkeeping — not header size — is the amplifier).
|
|
186
|
+
* It complements, and does not replace, the native header-count caps that a
|
|
187
|
+
* runtime/proxy terminating HTTP/2 must apply (NGINX `max_headers`, Node
|
|
188
|
+
* `server.maxHeadersCount`, etc.). Because the WHATWG `Headers` collection
|
|
189
|
+
* coalesces same-named fields, the count is over distinct header names —
|
|
190
|
+
* the truest signal available once a request has been normalised to a
|
|
191
|
+
* web-standard `Request`.
|
|
192
|
+
*
|
|
193
|
+
* A `limit` of `0` (or any non-positive / non-finite value) disables the
|
|
194
|
+
* check. Throws {@link RequestHeaderFieldsTooLargeError} (`431`) so the
|
|
195
|
+
* framework returns a structured `problem+json` response instead of routing
|
|
196
|
+
* a flood.
|
|
197
|
+
*
|
|
198
|
+
* @param headers - The incoming request headers.
|
|
199
|
+
* @param limit - Maximum distinct header fields to allow. `0` disables.
|
|
200
|
+
* @since 0.38.0
|
|
201
|
+
*/
|
|
202
|
+
export declare function assertHeaderCountWithinLimit(headers: Headers, limit: number): void;
|
|
169
203
|
/**
|
|
170
204
|
* Minimum acceptable secret length in bytes for HMAC / signing material in
|
|
171
205
|
* production (boot guard). Matches the OWASP "Secret Management"
|
package/dist/security.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - timingSafeEqual: length-independent string compare for fixed-length token checks.
|
|
9
9
|
* - randomId: cryptographically strong request id.
|
|
10
10
|
*/
|
|
11
|
-
import { PayloadTooLargeError, BadRequestError, } from "./errors.js";
|
|
11
|
+
import { PayloadTooLargeError, BadRequestError, RequestHeaderFieldsTooLargeError, } from "./errors.js";
|
|
12
12
|
// Resolved once at module load. Mirror of `DALOY_REQUEST_RAW_BODY` in
|
|
13
13
|
// app.ts; defined here via the global Symbol registry to avoid an import
|
|
14
14
|
// cycle (app.ts -> security.ts). Adapters attach a pre-validated
|
|
@@ -321,6 +321,53 @@ export function assertNoReservedInternalHeaders(headers) {
|
|
|
321
321
|
}
|
|
322
322
|
});
|
|
323
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Default cap on the number of distinct request header fields accepted
|
|
326
|
+
* before {@link assertHeaderCountWithinLimit} rejects the request. Chosen to
|
|
327
|
+
* sit far above any realistic legitimate request (browsers, tracing layers,
|
|
328
|
+
* and reverse proxies rarely add more than a few dozen headers) yet far
|
|
329
|
+
* below the thousands-of-headers floods used by header-count amplification
|
|
330
|
+
* attacks such as the "HTTP/2 Bomb".
|
|
331
|
+
*
|
|
332
|
+
* @since 0.38.0
|
|
333
|
+
*/
|
|
334
|
+
export const DEFAULT_MAX_HEADER_COUNT = 100;
|
|
335
|
+
/**
|
|
336
|
+
* Reject requests that carry more than `limit` distinct header fields.
|
|
337
|
+
*
|
|
338
|
+
* This is the runtime-portable, application-tier defence against
|
|
339
|
+
* header-*count* amplification (the dimension abused by the "HTTP/2 Bomb",
|
|
340
|
+
* where per-entry server bookkeeping — not header size — is the amplifier).
|
|
341
|
+
* It complements, and does not replace, the native header-count caps that a
|
|
342
|
+
* runtime/proxy terminating HTTP/2 must apply (NGINX `max_headers`, Node
|
|
343
|
+
* `server.maxHeadersCount`, etc.). Because the WHATWG `Headers` collection
|
|
344
|
+
* coalesces same-named fields, the count is over distinct header names —
|
|
345
|
+
* the truest signal available once a request has been normalised to a
|
|
346
|
+
* web-standard `Request`.
|
|
347
|
+
*
|
|
348
|
+
* A `limit` of `0` (or any non-positive / non-finite value) disables the
|
|
349
|
+
* check. Throws {@link RequestHeaderFieldsTooLargeError} (`431`) so the
|
|
350
|
+
* framework returns a structured `problem+json` response instead of routing
|
|
351
|
+
* a flood.
|
|
352
|
+
*
|
|
353
|
+
* @param headers - The incoming request headers.
|
|
354
|
+
* @param limit - Maximum distinct header fields to allow. `0` disables.
|
|
355
|
+
* @since 0.38.0
|
|
356
|
+
*/
|
|
357
|
+
export function assertHeaderCountWithinLimit(headers, limit) {
|
|
358
|
+
if (!(limit > 0) || !Number.isFinite(limit))
|
|
359
|
+
return;
|
|
360
|
+
let count = 0;
|
|
361
|
+
// Count via forEach (not keys()) so a same-named coalesced field counts
|
|
362
|
+
// once and the check works on any Headers-shaped object. Throwing from the
|
|
363
|
+
// callback bails the instant the cap is crossed, so a flood pays for at
|
|
364
|
+
// most `limit + 1` iterations rather than walking the whole set.
|
|
365
|
+
headers.forEach(() => {
|
|
366
|
+
if (++count > limit) {
|
|
367
|
+
throw new RequestHeaderFieldsTooLargeError(limit);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
324
371
|
/**
|
|
325
372
|
* Minimum acceptable secret length in bytes for HMAC / signing material in
|
|
326
373
|
* production (boot guard). Matches the OWASP "Secret Management"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -229,6 +229,7 @@
|
|
|
229
229
|
"devDependencies": {
|
|
230
230
|
"@hey-api/openapi-ts": "^0.97.1",
|
|
231
231
|
"@types/node": "^25.7.0",
|
|
232
|
+
"fast-check": "^3.23.2",
|
|
232
233
|
"prettier": "^3.8.3",
|
|
233
234
|
"tsx": "^4.22.3",
|
|
234
235
|
"typescript": "^6.0.3",
|
|
@@ -242,7 +243,7 @@
|
|
|
242
243
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
243
244
|
"coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=90 --test-coverage-functions=90 tests/**/*.test.ts",
|
|
244
245
|
"coverage:branches": "tsc -p tsconfig.coverage.json && node --test --experimental-test-coverage --test-coverage-include='dist-coverage/src/**' --test-coverage-branches=92 dist-coverage/tests/**/*.test.js",
|
|
245
|
-
"typecheck": "tsc --noEmit",
|
|
246
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json",
|
|
246
247
|
"format": "prettier --write .",
|
|
247
248
|
"gen:openapi": "node --import tsx scripts/dump-openapi.ts",
|
|
248
249
|
"gen:client": "openapi-ts",
|
|
@@ -261,6 +262,7 @@
|
|
|
261
262
|
"verify:no-leaked-credentials": "node --import tsx scripts/verify-no-leaked-credentials.ts",
|
|
262
263
|
"verify:no-leaky-agent-skills": "node --import tsx scripts/verify-no-leaky-agent-skills.ts",
|
|
263
264
|
"verify:no-toxic-agent-skills": "node --import tsx scripts/verify-no-toxic-agent-skills.ts",
|
|
265
|
+
"verify:no-agent-config-autorun": "node --import tsx scripts/verify-no-agent-config-autorun.ts",
|
|
264
266
|
"verify:no-invisible-unicode": "node --import tsx scripts/verify-no-invisible-unicode.ts",
|
|
265
267
|
"verify:no-encoded-payloads": "node --import tsx scripts/verify-no-encoded-payloads.ts",
|
|
266
268
|
"verify:no-registry-exfiltration": "node --import tsx scripts/verify-no-registry-exfiltration.ts",
|