@daloyjs/core 1.0.0-rc.2 → 1.0.0-rc.4

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 CHANGED
@@ -28,6 +28,12 @@ Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yello
28
28
 
29
29
  DaloyJS is maintained in the GitHub organization at <https://github.com/daloyjs>; the canonical framework repository is <https://github.com/daloyjs/daloy>.
30
30
 
31
+ ## Partners
32
+
33
+ <a href="https://snyk.io">
34
+ <img src="https://github.com/user-attachments/assets/da58db43-67cc-45d4-ade5-bdaa7b041465" alt="Snyk's Secure Developer Program" width="160">
35
+ </a>
36
+
31
37
  ---
32
38
 
33
39
  ## Built for the vibe-coding era
@@ -81,7 +87,7 @@ Each existing stack is excellent at one thing and forces tradeoffs everywhere el
81
87
 
82
88
  DaloyJS combines the wins:
83
89
 
84
- 1. **Explicit contracts, minimal ceremony.** One `app.route({...})` is the source of truth for validation, types, OpenAPI, the typed client, and contract tests.
90
+ 1. **Explicit contracts, minimal ceremony.** One `app.get(path, contract, handler)` (or the matching shorthand, or `app.route({...})` for reusable contracts) is the source of truth for validation, types, OpenAPI, the typed client, and contract tests.
85
91
  2. **One source of truth for validation, typing, and docs** via [Standard Schema](https://github.com/standard-schema/standard-schema) — Zod 4 / Valibot / ArkType / TypeBox all work, no lock-in.
86
92
  3. **Portable core, optional runtime optimizations** — the only thing the core knows is `Request → Response`. Adapters live at the edge.
87
93
  4. **Security guardrails by default — bad defaults are bugs.** The core enforces body limits, prototype-pollution-safe JSON, path-traversal rejection, request timeouts, content-type checks, and RFC 9457 problem+json errors with prod-mode redaction. First-party middleware covers Helmet-grade headers, CORS, CSRF, rate limits, request ids, and signed-cookie sessions.
@@ -174,28 +180,64 @@ app.use(requestId());
174
180
  app.use(secureHeaders());
175
181
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
176
182
 
177
- app.route({
178
- method: "GET",
179
- path: "/books/:id",
180
- operationId: "getBookById",
181
- tags: ["Books"],
182
- request: { params: z.object({ id: z.string() }) },
183
- responses: {
184
- 200: {
185
- description: "Found",
186
- body: z.object({ id: z.string(), title: z.string() }),
183
+ app.get(
184
+ "/books/:id",
185
+ {
186
+ tags: ["Books"],
187
+ request: { params: z.object({ id: z.string() }) },
188
+ responses: {
189
+ 200: {
190
+ description: "Found",
191
+ body: z.object({ id: z.string(), title: z.string() }),
192
+ },
193
+ 404: { description: "Not found" },
187
194
  },
188
- 404: { description: "Not found" },
189
195
  },
190
- handler: async ({ params }) => ({
196
+ async ({ params }) => ({
191
197
  status: 200,
192
198
  body: { id: params.id, title: `Book ${params.id}` },
193
199
  }),
194
- });
200
+ );
195
201
 
196
202
  serve(app, { port: 3000 });
197
203
  ```
198
204
 
205
+ For a concise route that keeps the full contract, use a method shorthand. The
206
+ method and path produce a stable operation id (`getRoot` here), while the
207
+ response schema preserves validation, OpenAPI, and data-exposure protection:
208
+
209
+ ```ts
210
+ const app = new App().get(
211
+ "/",
212
+ { responses: { 200: { body: z.object({ hello: z.string() }) } } },
213
+ () => ({ status: 200, body: { hello: "world" } })
214
+ );
215
+ ```
216
+
217
+ There is intentionally no `app.get(path, handler)` form. If a streaming or
218
+ proxy route genuinely needs to return a raw `Response`, declare a contract and
219
+ set `acknowledgeNoResponseBodySchema: true`; otherwise DaloyJS fails closed
220
+ rather than silently bypassing response-body validation. The same rule applies
221
+ when `preBody` or `beforeHandle` short-circuits with a successful raw response;
222
+ ordinary `4xx`/`5xx` hook denials remain secure by default without an opt-out.
223
+
224
+ Add request schemas and other contract options as the endpoint grows:
225
+
226
+ ```ts
227
+ app.post(
228
+ "/books",
229
+ {
230
+ request: { body: z.object({ title: z.string().min(1) }) },
231
+ responses: {
232
+ 201: {
233
+ body: z.object({ id: z.string(), title: z.string() }),
234
+ },
235
+ },
236
+ },
237
+ ({ body }) => ({ status: 201, body: { id: "1", title: body.title } })
238
+ );
239
+ ```
240
+
199
241
  ---
200
242
 
201
243
  ## OpenAPI + Hey API typed client
@@ -231,15 +273,32 @@ export default defineConfig({
231
273
  For TypeScript consumers in the same monorepo you can skip codegen entirely and use the **in-process typed client**:
232
274
 
233
275
  ```ts
234
- import { createClient } from "@daloyjs/core/client";
235
- const client = createClient(app, { baseUrl: "http://localhost:3000" });
276
+ import { createInProcessClient } from "@daloyjs/core/client";
277
+ const client = createInProcessClient(app);
236
278
  const r = await client.getBookById({ params: { id: "1" } });
237
279
  // ^? { status: 200; body: { id: string; title: string } } | { status: 404; ... }
238
280
  ```
239
281
 
240
- > Method inference relies on **chaining** your `app.route(...)` calls (`new App().route(a).route(b)`) and letting
241
- > TypeScript infer the variable's type. A widening `const app: App` annotation, a `: App` factory return type, or
242
- > registering routes as separate statements erases the per-route types and collapses the client to an untyped surface.
282
+ For multi-file applications, export each contract with `defineRoute()` and
283
+ compose the literal tuple with `app.registerRoutes([...])`. This retains every
284
+ operation across module boundaries:
285
+
286
+ ```ts
287
+ import { App, defineRoute } from "@daloyjs/core";
288
+ import { createInProcessClient } from "@daloyjs/core/client";
289
+ import { listBooksRoute } from "./routes/list-books.js";
290
+ import { getBookRoute } from "./routes/get-book.js";
291
+
292
+ const app = new App().registerRoutes([listBooksRoute, getBookRoute] as const);
293
+ const client = createInProcessClient(app); // typed, no socket or port
294
+ ```
295
+
296
+ Chained `route()` calls remain supported. Avoid widening a composed app back to
297
+ a bare `App` annotation, because that deliberately discards its route tuple.
298
+ Callback-style `group()` and plugin `register()` provide runtime encapsulation,
299
+ but TypeScript cannot widen the already-created parent variable from inside a
300
+ callback. Export route tuples and compose them with `registerRoutes()` whenever
301
+ the no-codegen client must include those module routes.
243
302
 
244
303
  ---
245
304
 
@@ -323,7 +382,10 @@ import { generateOpenAPI } from "@daloyjs/core/openapi";
323
382
 
324
383
  The UI is always contract-accurate — never stale. `create-daloy` templates opt in with `docs: true`.
325
384
 
326
- If you omit `openapi.info.title` / `info.version`, Daloy reads your project's `package.json` (`name`, `version`, `description`) automatically — no boilerplate. Deno projects without a `package.json` fall back to `deno.json` / `deno.jsonc`. Explicit values always win.
385
+ If you omit `openapi.info`, the portable defaults are `DaloyJS API` / `0.0.0`.
386
+ Set `openapi.info` (or the top-level `title`, `version`, and `description`) for
387
+ real services. The core never reads the host filesystem, so the same docs
388
+ bundle runs unchanged on Workers, Vercel, Bun, Deno, and Node.
327
389
 
328
390
  Prefer a factory? `createApp(options)` is exported as an alias of `new App(options)`.
329
391
 
@@ -459,13 +521,14 @@ Gate it in CI two ways: `daloy inspect --check <entry>` exits non-zero on any er
459
521
  const usersPlugin = {
460
522
  name: "users",
461
523
  register(app) {
462
- app.route({
463
- method: "GET",
464
- path: "/me",
465
- operationId: "me",
466
- responses: { 200: { description: "ok" } },
467
- handler: async () => ({ status: 200, body: { user: "alice" } }),
468
- });
524
+ app.get(
525
+ "/me",
526
+ {
527
+ operationId: "me",
528
+ responses: { 200: { description: "ok" } },
529
+ },
530
+ async () => ({ status: 200, body: { user: "alice" } }),
531
+ );
469
532
  },
470
533
  };
471
534
  app.register(usersPlugin, { prefix: "/users", tags: ["Users"] });
@@ -509,7 +572,7 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
509
572
 
510
573
  ## Status
511
574
 
512
- DaloyJS is at **`1.0.0-rc.2`**, a security-hardening release candidate. Because the framework has no external users yet, this RC ships a few intentional breaking changes (see the [CHANGELOG](CHANGELOG.md)) to get the secure-by-default posture right before GA rather than deferring them; the generated OpenAPI contract is unchanged. From `1.0.0` GA onward, breaking changes follow SemVer with deprecations getting at least one minor cycle. The framework is already in use for production trials.
575
+ DaloyJS is at **`1.0.0-rc.4`**, a security-hardening release candidate. Because the framework has no external users yet, this RC makes a few intentional changes (see the [CHANGELOG](CHANGELOG.md)) to get the secure-by-default posture right before the stable release rather than deferring them; the generated OpenAPI contract is unchanged. From `1.0.0` stable onward, the API follows SemVer with deprecations getting at least one minor cycle. The framework is already in use for production trials.
513
576
 
514
577
  **Release quality bar.** Every release ships with **≥90% line + function coverage and ≥90% branch coverage**, strict TypeScript, OpenSSF Scorecard, CodeQL + Opengrep dual SAST, zizmor workflow linting, and npm provenance. Coverage was relaxed from a former 100% gate so complex security work isn't blocked chasing throwaway tests for unreachable defensive branches or tsx source-map phantoms; see [AGENTS.md](AGENTS.md) for the policy.
515
578
 
@@ -517,7 +580,10 @@ DaloyJS is at **`1.0.0-rc.2`**, a security-hardening release candidate. Because
517
580
 
518
581
  - Contract-first routing with Standard Schema validation (Zod 4, Valibot, ArkType, TypeBox) and OpenAPI 3.1 generated from a single source of truth.
519
582
  - Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true`, with a choice of Scalar (default), Swagger UI, or Redoc via `docs.ui`, plus Scalar theming/custom CSS/auth defaults via `docs.scalar`, Swagger UI options via `docs.swagger` (including persisted Authorize credentials), Redoc options via `docs.redoc`, and a provider-neutral `docs.auth` launcher for local login routes or third-party OAuth2/OIDC providers.
520
- - Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno); explicit `openapi.info` values always win.
583
+ - Filesystem-free OpenAPI metadata: explicit `openapi.info` / top-level metadata wins, with portable `DaloyJS API` / `0.0.0` fallbacks on every runtime.
584
+ - Contract-required `get()` / `post()` / `put()` / `patch()` / `delete()` / `head()` shorthands with deterministic method+path operation IDs and the same validation, OpenAPI, response-exposure protection, and client typing as `route()`; there is no silent two-argument schema bypass.
585
+ - Multi-file contract composition through `defineRoute()` + `app.registerRoutes([...])`, plus a typed `createInProcessClient(app)` that traverses the real pipeline without a socket.
586
+ - Header/JWT/basic/mTLS authentication runs in the `preBody` phase before request-body I/O; body-aware WAF, idempotency, signature, and application middleware keep the validated `beforeHandle` phase.
521
587
  - RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
522
588
  - AI-friendly route metadata via optional `meta: { examples, extensions, summary, description, tags }`; examples are validated against your schemas at build time, surfaced as OpenAPI `examples` + `x-daloy-*` extensions, and dumped as `routes.json` / `routes.yaml` via `daloy inspect --ai`.
523
589
  - Dependency-free MCP Streamable HTTP server helpers at `@daloyjs/core/mcp`: `createMcpHandler()` exposes tools (with `outputSchema`, `annotations`, and icons), resources, RFC 6570 resource templates, and prompts (with required-argument enforcement) over JSON-RPC 2.0 and validates `Origin` against DNS rebinding (with an `allowedOrigins` allowlist), while `mcpRoutes("/mcp", handler)` mounts the POST / GET / OPTIONS Daloy routes — with the JSON-RPC envelope schema surfaced in OpenAPI — for a dedicated MCP service with the same auth, rate-limit, body-limit, and timeout middleware as any other app. Every `tools/call` argument is validated server-side against the tool's `inputSchema` (a dependency-free JSON-Schema subset — `type`/`required`/`properties`/`additionalProperties`/`enum`/`const`/bounds; exposed as `validateMcpInput()`) before the handler runs, rejecting a mismatch with JSON-RPC `-32602`; the JSON-RPC body is parsed with prototype-pollution-safe `safeJsonParse`; and an unauthenticated `mcpRoutes()` endpoint refuses to boot in production unless opted out with `mcpRoutes(path, handler, { public: true })`.
@@ -594,7 +660,7 @@ The framework refuses to start (or to construct) when configuration is unsafe:
594
660
  - `concurrencyLimit()` per-route / per-client concurrency limits + queueing at `@daloyjs/core/concurrency-limit`: HAProxy `maxconn`/queue parity at the app layer. Bounds in-flight requests through a surface with a per-bucket semaphore (`maxConcurrent`), a bounded FIFO queue (`maxQueue`) with an optional `queueTimeoutMs`, and a fast `503` + `Retry-After` once the queue is full or the wait times out. Partition the budget with `scope`: `"global"` (default), `"route"` (per `method + path`), `"client"` (per identity, needs `trustProxyHeaders`/`keyGenerator`), or a custom function (`undefined` skips limiting, fail-open). Acquires in `beforeHandle` and releases in `onSend`, so slots are freed on success, error, and short-circuit paths alike — never leaked. `onReject` observability hook, configurable `retryAfterSeconds`/`message`. Complements the `maxConnections` socket cap and `loadShedding()`. Zero runtime dependencies. HAProxy `maxconn`/queue parity at the app layer. Bounds in-flight requests through a surface with a per-bucket semaphore (`maxConcurrent`), a bounded FIFO queue (`maxQueue`) with an optional `queueTimeoutMs`, and a fast `503` + `Retry-After` once the queue is full or the wait times out. Partition the budget with `scope`: `"global"` (default), `"route"` (per `method + path`), `"client"` (per identity, needs `trustProxyHeaders`/`keyGenerator`), or a custom function (`undefined` skips limiting, fail-open). Acquires in `beforeHandle` and releases in `onSend`, so slots are freed on success, error, and short-circuit paths alike — never leaked. `onReject` observability hook, configurable `retryAfterSeconds`/`message`. Complements the `maxConnections` socket cap and `loadShedding()`. Zero runtime dependencies.
595
661
  - `requestDecompression()` inbound decompression-bomb guard at `@daloyjs/core/request-decompression`: core is safe by omission (it never decompresses request bodies), so this is the opt-in middleware for services that must accept compressed uploads. Inflates `gzip` / `deflate` bodies behind two caps enforced **during** inflation so a zip bomb is aborted before it is fully materialised: an absolute `maxDecompressedBytes` (required) and an expansion-ratio `maxRatio` (default `100`), both rejecting with `413`. The compressed upload itself is bounded by `maxCompressedBytes` (default 1 MiB) before a byte is inflated. Unknown, non-allowlisted, runtime-unsupported, or **layered** (`gzip, gzip`) encodings are refused `415`; malformed streams `400`; bodyless / uncompressed / `identity` / `GET` / `HEAD` traffic passes through untouched. Runs in `onRequest` and stashes the inflated bytes so schema-validated bodies and raw-body handlers both see the decompressed payload. `onBomb` observability hook, exported `decompressRequestBody()` for custom flows. Built on web-standard `DecompressionStream` (brotli excluded — not in the spec). Zero runtime dependencies.
596
662
  - `waf()` opt-in WAF-lite signature/anomaly inbound-inspection middleware at `@daloyjs/core/waf`: a first-party defense-in-depth layer for teams without an edge WAF (it does **not** replace ModSecurity / a CDN WAF). Wires DaloyJS' high-confidence injection signatures — SQLi, XSS, NoSQL-operator injection (reusing `hasMongoOperatorKeys` for a structural body check), and command injection — into a single scored inbound-inspection pass over the decoded path, the raw + decoded query string, an opt-in header allowlist, and the validated body. Each rule that fires adds an anomaly `score`; reaching `blockThreshold` (default `5`) rejects with a generic `403` (block mode) or merely reports via `onMatch` (log mode) so operators can tune against real traffic first. Per-rule enable/disable + score overrides, inspection-surface toggles, control-character-stripped log samples, and bounded scanning (`maxValueLength` / `maxBodyNodes`) keep a hostile payload from becoming CPU-DoS. The `403` body never names the rule that fired. Zero runtime dependencies.
597
- - Built-in docs UI Subresource Integrity (SRI): `DocsAssetOptions` lets `scalarHtml()` / `swaggerUiHtml()` / `redocHtml()` and the `docs: { assets }` auto-mount pin version-exact `*Integrity` hashes (`sha256`/`sha384`/`sha512`) plus a `crossOrigin` value (default `"anonymous"`) on the CDN-loaded Scalar / Swagger UI / Redoc `<script>` / `<link>` tags, so a poisoned jsDelivr asset can't execute. Malformed SRI values throw a `TypeError` at startup (browsers ignore unparseable `integrity`, so failing loud avoids a false sense of protection); self-hosting the assets via the same `assets` URLs stays supported. Zero runtime dependencies.
663
+ - Built-in docs UI Subresource Integrity (SRI): the default Scalar / Swagger UI / Redoc / AsyncAPI assets use version-exact URLs with matching SHA-384 hashes and `crossorigin="anonymous"`, so a poisoned CDN asset cannot execute. `DocsAssetOptions` supports validated URL/hash overrides or self-hosting; malformed SRI values throw a `TypeError` instead of silently weakening the page. Zero runtime dependencies.
598
664
  - HTTP Message Signatures (RFC 9421) at `@daloyjs/core/http-signatures`: first-party sign/verify for server-to-server request authentication via the standard `Signature` / `Signature-Input` headers — complements the inbound-only webhook HMAC and `clientCertAuth()` mTLS. `signMessage()` / `signRequest()` build an RFC 9421 signature base over derived components (`@method`, `@target-uri`, `@authority`, `@scheme`, `@request-target`, `@path`, `@query`, `@query-param`, `@status`) and HTTP fields with Structured-Fields header serialization; `verifyMessage()` / `verifyRequest()` and the `httpSignatureAuth()` middleware check them. Algorithms `hmac-sha256` / `ed25519` / `ecdsa-p256-sha256` / `ecdsa-p384-sha384` / `rsa-pss-sha512` / `rsa-v1_5-sha256` via WebCrypto (no `node:` imports). Secure-by-default verify: a **mandatory `algorithms` allowlist** (no implicit "any alg"), optional per-key alg pinning to defeat algorithm-confusion, a required `created` timestamp with a 300s freshness window, `created`-in-future / `expires` skew rejection, configurable `requiredComponents`, a 32-byte raw-HMAC floor, a 2048-bit RSA modulus floor (NIST SP 800-131A, parity with the JWT verifier), and `nonce` replay defense; the middleware answers a missing/invalid signature with `401` + `Cache-Control: no-store` and stamps the verified result on `ctx.state.httpSignature`. Ships RFC 9530 `contentDigest()` / `verifyContentDigest()` to bind the request body. Zero runtime dependencies.
599
665
  - `compression()` built on web-standard `CompressionStream` (prefers `br` > `gzip` > `deflate`), with BREACH-aware always-on guards (skips `Set-Cookie`, `Authorization`, session / CSRF cookies, already-compressed content types), `minimumSize: 1024`, negative-compression-ratio post-check, no configurable `compressLevel` knob (CPU-DoS defense — `level: 9` is refused at construction), always-on `Vary: Accept-Encoding`, and strong → weak ETag downgrade per RFC 9110 §8.8.3.
600
666
  - `etag()` helper auto-skips on `Set-Cookie` and private / no-store / no-cache `Cache-Control` (cross-tenant fingerprinting defense).
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Vercel / web-standard handler.
3
3
  *
4
- * Vercel now recommends the Node.js runtime over Edge for new functions. The
5
- * runtime is web-standard, but the export shape differs by integration: Node
6
- * `/api` functions use a default `{ fetch }` object, while Edge functions use a
7
- * bare function export. If you are hosting a DaloyJS app inside an existing
8
- * Next.js app, App Router route handlers use named method exports.
4
+ * Vercel recommends the Node.js runtime for new functions (it runs on Fluid
5
+ * Compute with full Node APIs) and has deprecated standalone Edge Functions.
6
+ * The runtime is web-standard, but the export shape differs by integration:
7
+ * Node `/api` functions use a default `{ fetch }` object, App Router route
8
+ * handlers use named method exports, and the deprecated Edge runtime expects a
9
+ * bare function export — {@link toWebHandler} — plus `export const runtime =
10
+ * "edge"`.
9
11
  *
10
- * // Vercel Functions (`api/[...path].ts`)
12
+ * // Vercel Functions (`api/[...path].ts`) — recommended
11
13
  * import { toFetchHandler } from "@daloyjs/core/vercel";
12
14
  * export default toFetchHandler(app);
13
15
  *
@@ -15,8 +17,6 @@
15
17
  * import { toRouteHandlers } from "@daloyjs/core/vercel";
16
18
  * export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD } =
17
19
  * toRouteHandlers(app);
18
- *
19
- * `toEdgeHandler` is kept as a backward-compatible alias of `toWebHandler`.
20
20
  */
21
21
  import type { App } from "../app.js";
22
22
  /** Web-standard handler shape used by Vercel Functions, Next.js route handlers, and middleware. */
@@ -44,8 +44,6 @@ export declare function toWebHandler(app: App): WebHandler;
44
44
  * @returns A {@link FetchHandler} object suitable as the module's `export default`.
45
45
  */
46
46
  export declare function toFetchHandler(app: App): FetchHandler;
47
- /** Backward-compatible alias for {@link toWebHandler}. */
48
- export declare const toEdgeHandler: typeof toWebHandler;
49
47
  /**
50
48
  * Build the `{ GET, POST, ... }` object expected by Next.js App Router
51
49
  * `route.ts` files when a DaloyJS app is mounted inside an existing Next app.
@@ -18,8 +18,6 @@ export function toWebHandler(app) {
18
18
  export function toFetchHandler(app) {
19
19
  return { fetch: toWebHandler(app) };
20
20
  }
21
- /** Backward-compatible alias for {@link toWebHandler}. */
22
- export const toEdgeHandler = toWebHandler;
23
21
  /**
24
22
  * Build the `{ GET, POST, ... }` object expected by Next.js App Router
25
23
  * `route.ts` files when a DaloyJS app is mounted inside an existing Next app.
package/dist/app.d.ts CHANGED
@@ -93,6 +93,29 @@ export interface AppOptions {
93
93
  * @since 0.38.0
94
94
  */
95
95
  maxHeaderCount?: number;
96
+ /**
97
+ * Maximum number of keys (summed across every object in the tree) permitted
98
+ * when parsing a JSON request body. This bounds "hash-flood" / wide-object
99
+ * attacks that easily fit inside `bodyLimitBytes` (e.g. 40 000 tiny keys).
100
+ * Applies to top-level objects and all nested objects. Set to 0 to disable.
101
+ * Default: 10 000.
102
+ *
103
+ * Exposed in `getSecurityPosture()` and audited by `daloy doctor`.
104
+ *
105
+ * @since 1.0.0
106
+ */
107
+ jsonMaxKeys?: number;
108
+ /**
109
+ * Maximum nesting depth permitted for JSON request bodies (objects and arrays).
110
+ * Prevents deeply-nested structures that can consume excessive CPU/memory
111
+ * during schema validation or handler processing. Set to 0 to disable.
112
+ * Default: 50.
113
+ *
114
+ * Exposed in `getSecurityPosture()` and audited by `daloy doctor`.
115
+ *
116
+ * @since 1.0.0
117
+ */
118
+ jsonMaxDepth?: number;
96
119
  /**
97
120
  * Per-request limits applied when parsing `multipart/form-data` bodies.
98
121
  * These run in addition to `bodyLimitBytes`. Use them to cap the size of
@@ -516,7 +539,7 @@ export interface PluginExtension {
516
539
  /** Unique extension name. Referenced by `before` / `after` on siblings. */
517
540
  name: string;
518
541
  /** Lifecycle event the handler attaches to. */
519
- event: "onRequest" | "beforeHandle" | "afterHandle" | "onSend" | "onError";
542
+ event: "onRequest" | "preBody" | "beforeHandle" | "afterHandle" | "onSend" | "onError";
520
543
  /** Hook handler. The shape mirrors the matching {@link Hooks} entry. */
521
544
  handler: (...args: any[]) => any;
522
545
  /** Extension names this one must run before. */
@@ -828,6 +851,18 @@ export declare const DALOY_LIGHT_RESPONSE_OK: unique symbol;
828
851
  * @typeParam R - The route definition being registered.
829
852
  */
830
853
  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];
854
+ /** Append a literal tuple of route contracts to an App's accumulated routes. */
855
+ type AppendRoutes<Routes extends readonly RouteDefinition<any, any, any, any>[], Added extends readonly RouteDefinition<any, any, any, any>[]> = readonly RouteDefinition<any, any, any, any>[] extends Routes ? Added : readonly [...Routes, ...Added];
856
+ type PascalWords<S extends string> = S extends `${infer Head}-${infer Tail}` ? `${PascalWords<Head>}${PascalWords<Tail>}` : S extends `${infer Head}_${infer Tail}` ? `${Capitalize<Head>}${PascalWords<Tail>}` : Capitalize<S>;
857
+ type OperationSegment<S extends string> = S extends `:${infer Param}` ? `By${PascalWords<Param>}` : PascalWords<S>;
858
+ type OperationPathTail<P extends string> = P extends `${infer Segment}/${infer Rest}` ? `${OperationSegment<Segment>}${OperationPathTail<Rest>}` : OperationSegment<P>;
859
+ type AutoOperationId<M extends HttpMethod, P extends PathString> = `${Lowercase<M>}${P extends "/" ? "Root" : P extends `/${infer Tail}` ? OperationPathTail<Tail> : never}`;
860
+ type ShorthandOptions<P extends PathString, M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap, Op extends string | undefined> = Omit<RouteDefinition<P, M, Req, Res>, "method" | "path" | "operationId" | "handler"> & {
861
+ operationId?: Op;
862
+ };
863
+ type ShorthandRoute<P extends PathString, M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap, Op extends string | undefined> = RouteDefinition<P, M, Req, Res> & {
864
+ operationId: Op extends string ? Op : AutoOperationId<M, P>;
865
+ };
831
866
  /**
832
867
  * The DaloyJS application: a contract-first router plus a web-standard
833
868
  * `fetch(Request): Promise<Response>` handler that runs unchanged on Node,
@@ -1007,6 +1042,8 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
1007
1042
  bodyLimitBytes: number;
1008
1043
  requestTimeoutMs: number;
1009
1044
  maxHeaderCount: number;
1045
+ jsonMaxKeys: number;
1046
+ jsonMaxDepth: number;
1010
1047
  stripServerHeaders: boolean;
1011
1048
  production: boolean;
1012
1049
  };
@@ -1204,6 +1241,74 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
1204
1241
  }): App<AppendRoute<Routes, RouteDefinition<P, M, Req, Res> & {
1205
1242
  operationId: Op;
1206
1243
  }>>;
1244
+ /**
1245
+ * Register a literal tuple of independently defined route contracts.
1246
+ *
1247
+ * Unlike repeated statements against an already-declared `App` variable,
1248
+ * this method returns an App whose route tuple includes every supplied
1249
+ * contract. That preserves the exact no-codegen client surface across route
1250
+ * files and feature modules.
1251
+ *
1252
+ * @param definitions - Readonly literal tuple of route definitions.
1253
+ * @returns This App instance widened with every supplied route contract.
1254
+ * @since 1.0.0
1255
+ */
1256
+ registerRoutes<const Added extends readonly RouteDefinition<any, any, any, any>[]>(definitions: Added): App<AppendRoutes<Routes, Added>>;
1257
+ /**
1258
+ * Register a contract-backed `GET` route with validation and typed responses.
1259
+ * @param path - Literal route path.
1260
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1261
+ * @param handler - Handler contextually typed from the contract options.
1262
+ * @returns This App widened with the registered route.
1263
+ * @since 1.0.0
1264
+ */
1265
+ get<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "GET", Req, Res, Op>, handler: RouteDefinition<P, "GET", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "GET", Req, Res, Op>>>;
1266
+ /**
1267
+ * Register a contract-backed `POST` route with validation and typed responses.
1268
+ * @param path - Literal route path.
1269
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1270
+ * @param handler - Handler contextually typed from the contract options.
1271
+ * @returns This App widened with the registered route.
1272
+ * @since 1.0.0
1273
+ */
1274
+ post<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "POST", Req, Res, Op>, handler: RouteDefinition<P, "POST", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "POST", Req, Res, Op>>>;
1275
+ /**
1276
+ * Register a contract-backed `PUT` route with validation and typed responses.
1277
+ * @param path - Literal route path.
1278
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1279
+ * @param handler - Handler contextually typed from the contract options.
1280
+ * @returns This App widened with the registered route.
1281
+ * @since 1.0.0
1282
+ */
1283
+ put<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "PUT", Req, Res, Op>, handler: RouteDefinition<P, "PUT", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "PUT", Req, Res, Op>>>;
1284
+ /**
1285
+ * Register a contract-backed `PATCH` route with validation and typed responses.
1286
+ * @param path - Literal route path.
1287
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1288
+ * @param handler - Handler contextually typed from the contract options.
1289
+ * @returns This App widened with the registered route.
1290
+ * @since 1.0.0
1291
+ */
1292
+ patch<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "PATCH", Req, Res, Op>, handler: RouteDefinition<P, "PATCH", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "PATCH", Req, Res, Op>>>;
1293
+ /**
1294
+ * Register a contract-backed `DELETE` route with typed response statuses.
1295
+ * @param path - Literal route path.
1296
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1297
+ * @param handler - Handler contextually typed from the contract options.
1298
+ * @returns This App widened with the registered route.
1299
+ * @since 1.0.0
1300
+ */
1301
+ delete<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "DELETE", Req, Res, Op>, handler: RouteDefinition<P, "DELETE", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "DELETE", Req, Res, Op>>>;
1302
+ /**
1303
+ * Register an explicit contract-backed `HEAD` route.
1304
+ * @param path - Literal route path.
1305
+ * @param options - Route schemas, responses, metadata, hooks, and security.
1306
+ * @param handler - Handler contextually typed from the contract options.
1307
+ * @returns This App widened with the registered route.
1308
+ * @since 1.0.0
1309
+ */
1310
+ head<const P extends PathString, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(path: P, options: ShorthandOptions<P, "HEAD", Req, Res, Op>, handler: RouteDefinition<P, "HEAD", Req, Res>["handler"]): App<AppendRoute<Routes, ShorthandRoute<P, "HEAD", Req, Res, Op>>>;
1311
+ private addHttpShorthand;
1207
1312
  /**
1208
1313
  * Register a WebSocket route. The handler runs when an HTTP client sends an
1209
1314
  * `Upgrade: websocket` request to `path`; the adapter performs the RFC 6455
@@ -1401,6 +1506,25 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
1401
1506
  * });
1402
1507
  * ```
1403
1508
  *
1509
+ * ### Scoping (Fastify-style encapsulation)
1510
+ *
1511
+ * Decorations are **scoped to the app instance they are declared on**, and
1512
+ * are captured per route at registration time:
1513
+ *
1514
+ * - Calling `decorate()` on the root app makes the value visible to every
1515
+ * route, including routes inside plugins/groups registered *afterwards*
1516
+ * (app-level decorations flow inward).
1517
+ * - Calling `decorate()` on the child app passed to {@link App.register} /
1518
+ * {@link App.group} scopes the value to **that plugin's routes only** — it
1519
+ * does not leak to sibling plugins or back to the root.
1520
+ *
1521
+ * Each route binds to its scope's decorations when it is registered, so
1522
+ * **decorate before registering the routes that consume the value** (the same
1523
+ * ordering Fastify requires). Adding a decoration to a scope that already had
1524
+ * at least one is picked up by that scope's existing routes; but the first
1525
+ * decoration added to a scope *after* its routes were registered will not
1526
+ * reach them.
1527
+ *
1404
1528
  * @param key - Property name on `ctx.state`.
1405
1529
  * @param value - Value bound to that property on every request.
1406
1530
  * @param opts - Pass `{ override: true }` to replace an existing decoration (logged as a warning).
@@ -1652,11 +1776,4 @@ export declare function findRoutesMissingResponseBodySchema(routes: readonly Pic
1652
1776
  * @since 0.3.0
1653
1777
  */
1654
1778
  export declare function createApp(options?: AppOptions): App;
1655
- /**
1656
- * Test helper: clear the cached package.json read so each test starts
1657
- * from a fresh lookup. Not part of the public API.
1658
- *
1659
- * @internal
1660
- */
1661
- export declare function _resetPackageJsonCacheForTests(): void;
1662
1779
  export {};