@daloyjs/core 1.0.0-rc.3 → 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 +90 -30
- package/dist/app.d.ts +106 -8
- package/dist/app.js +193 -179
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +28 -11
- package/dist/client.js +29 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/internal-response.d.ts +15 -0
- package/dist/internal-response.js +27 -0
- package/dist/jwk.d.ts +11 -7
- package/dist/jwk.js +11 -7
- package/dist/mcp.js +11 -6
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +96 -40
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +3 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/safe-redirect.d.ts +4 -1
- package/dist/safe-redirect.js +4 -1
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +21 -0
- package/dist/security.js +89 -0
- package/dist/tenancy.d.ts +2 -2
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ Each existing stack is excellent at one thing and forces tradeoffs everywhere el
|
|
|
87
87
|
|
|
88
88
|
DaloyJS combines the wins:
|
|
89
89
|
|
|
90
|
-
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.
|
|
91
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.
|
|
92
92
|
3. **Portable core, optional runtime optimizations** — the only thing the core knows is `Request → Response`. Adapters live at the edge.
|
|
93
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.
|
|
@@ -180,28 +180,64 @@ app.use(requestId());
|
|
|
180
180
|
app.use(secureHeaders());
|
|
181
181
|
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
182
182
|
|
|
183
|
-
app.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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" },
|
|
193
194
|
},
|
|
194
|
-
404: { description: "Not found" },
|
|
195
195
|
},
|
|
196
|
-
|
|
196
|
+
async ({ params }) => ({
|
|
197
197
|
status: 200,
|
|
198
198
|
body: { id: params.id, title: `Book ${params.id}` },
|
|
199
199
|
}),
|
|
200
|
-
|
|
200
|
+
);
|
|
201
201
|
|
|
202
202
|
serve(app, { port: 3000 });
|
|
203
203
|
```
|
|
204
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
|
+
|
|
205
241
|
---
|
|
206
242
|
|
|
207
243
|
## OpenAPI + Hey API typed client
|
|
@@ -237,15 +273,32 @@ export default defineConfig({
|
|
|
237
273
|
For TypeScript consumers in the same monorepo you can skip codegen entirely and use the **in-process typed client**:
|
|
238
274
|
|
|
239
275
|
```ts
|
|
240
|
-
import {
|
|
241
|
-
const client =
|
|
276
|
+
import { createInProcessClient } from "@daloyjs/core/client";
|
|
277
|
+
const client = createInProcessClient(app);
|
|
242
278
|
const r = await client.getBookById({ params: { id: "1" } });
|
|
243
279
|
// ^? { status: 200; body: { id: string; title: string } } | { status: 404; ... }
|
|
244
280
|
```
|
|
245
281
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
249
302
|
|
|
250
303
|
---
|
|
251
304
|
|
|
@@ -329,7 +382,10 @@ import { generateOpenAPI } from "@daloyjs/core/openapi";
|
|
|
329
382
|
|
|
330
383
|
The UI is always contract-accurate — never stale. `create-daloy` templates opt in with `docs: true`.
|
|
331
384
|
|
|
332
|
-
If you omit `openapi.info
|
|
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.
|
|
333
389
|
|
|
334
390
|
Prefer a factory? `createApp(options)` is exported as an alias of `new App(options)`.
|
|
335
391
|
|
|
@@ -465,13 +521,14 @@ Gate it in CI two ways: `daloy inspect --check <entry>` exits non-zero on any er
|
|
|
465
521
|
const usersPlugin = {
|
|
466
522
|
name: "users",
|
|
467
523
|
register(app) {
|
|
468
|
-
app.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
524
|
+
app.get(
|
|
525
|
+
"/me",
|
|
526
|
+
{
|
|
527
|
+
operationId: "me",
|
|
528
|
+
responses: { 200: { description: "ok" } },
|
|
529
|
+
},
|
|
530
|
+
async () => ({ status: 200, body: { user: "alice" } }),
|
|
531
|
+
);
|
|
475
532
|
},
|
|
476
533
|
};
|
|
477
534
|
app.register(usersPlugin, { prefix: "/users", tags: ["Users"] });
|
|
@@ -515,7 +572,7 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
|
|
|
515
572
|
|
|
516
573
|
## Status
|
|
517
574
|
|
|
518
|
-
DaloyJS is at **`1.0.0-rc.
|
|
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.
|
|
519
576
|
|
|
520
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.
|
|
521
578
|
|
|
@@ -523,7 +580,10 @@ DaloyJS is at **`1.0.0-rc.3`**, a security-hardening release candidate. Because
|
|
|
523
580
|
|
|
524
581
|
- Contract-first routing with Standard Schema validation (Zod 4, Valibot, ArkType, TypeBox) and OpenAPI 3.1 generated from a single source of truth.
|
|
525
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.
|
|
526
|
-
-
|
|
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.
|
|
527
587
|
- RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
|
|
528
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`.
|
|
529
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 })`.
|
|
@@ -600,7 +660,7 @@ The framework refuses to start (or to construct) when configuration is unsafe:
|
|
|
600
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.
|
|
601
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.
|
|
602
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.
|
|
603
|
-
- Built-in docs UI Subresource Integrity (SRI):
|
|
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.
|
|
604
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.
|
|
605
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.
|
|
606
666
|
- `etag()` helper auto-skips on `Set-Cookie` and private / no-store / no-cache `Cache-Control` (cross-tenant fingerprinting defense).
|
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
|
|
@@ -1671,11 +1776,4 @@ export declare function findRoutesMissingResponseBodySchema(routes: readonly Pic
|
|
|
1671
1776
|
* @since 0.3.0
|
|
1672
1777
|
*/
|
|
1673
1778
|
export declare function createApp(options?: AppOptions): App;
|
|
1674
|
-
/**
|
|
1675
|
-
* Test helper: clear the cached package.json read so each test starts
|
|
1676
|
-
* from a fresh lookup. Not part of the public API.
|
|
1677
|
-
*
|
|
1678
|
-
* @internal
|
|
1679
|
-
*/
|
|
1680
|
-
export declare function _resetPackageJsonCacheForTests(): void;
|
|
1681
1779
|
export {};
|