@daloyjs/core 0.35.0 → 0.35.2
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 +145 -29
- package/dist/adapters/bun.d.ts +0 -1
- package/dist/adapters/bun.js +0 -1
- package/dist/adapters/cloudflare.d.ts +0 -1
- package/dist/adapters/cloudflare.js +0 -1
- package/dist/adapters/deno.d.ts +0 -1
- package/dist/adapters/deno.js +0 -1
- package/dist/adapters/fastly.d.ts +0 -1
- package/dist/adapters/fastly.js +0 -1
- package/dist/adapters/lambda.d.ts +0 -1
- package/dist/adapters/lambda.js +0 -1
- package/dist/adapters/node.d.ts +11 -1
- package/dist/adapters/node.js +246 -44
- package/dist/adapters/vercel.d.ts +0 -1
- package/dist/adapters/vercel.js +0 -1
- package/dist/app.d.ts +52 -1
- package/dist/app.js +557 -146
- package/dist/banner.d.ts +0 -1
- package/dist/banner.js +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/combine.d.ts +0 -1
- package/dist/combine.js +0 -1
- package/dist/compression.d.ts +0 -1
- package/dist/compression.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/conn-info.d.ts +0 -1
- package/dist/conn-info.js +0 -1
- package/dist/contract.d.ts +0 -1
- package/dist/contract.js +0 -1
- package/dist/cookie.d.ts +0 -1
- package/dist/cookie.js +0 -1
- package/dist/dependency.d.ts +0 -1
- package/dist/dependency.js +0 -1
- package/dist/discriminator.d.ts +0 -1
- package/dist/discriminator.js +0 -1
- package/dist/docs.d.ts +0 -1
- package/dist/docs.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +6 -3
- package/dist/etag.d.ts +0 -1
- package/dist/etag.js +0 -1
- package/dist/fetch-guard.d.ts +0 -1
- package/dist/fetch-guard.js +0 -1
- package/dist/hashing.d.ts +0 -1
- package/dist/hashing.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/ip-restriction.d.ts +0 -1
- package/dist/ip-restriction.js +0 -1
- package/dist/jwk.d.ts +0 -1
- package/dist/jwk.js +0 -1
- package/dist/jwt.d.ts +0 -1
- package/dist/jwt.js +0 -1
- package/dist/load-shedding.d.ts +0 -1
- package/dist/load-shedding.js +0 -1
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +0 -1
- package/dist/middleware.d.ts +0 -1
- package/dist/middleware.js +16 -13
- package/dist/multipart.d.ts +0 -1
- package/dist/multipart.js +0 -1
- package/dist/openapi.d.ts +0 -1
- package/dist/openapi.js +0 -1
- package/dist/rate-limit-redis.d.ts +0 -1
- package/dist/rate-limit-redis.js +0 -1
- package/dist/router.d.ts +0 -1
- package/dist/router.js +0 -1
- package/dist/safe-redirect.d.ts +2 -3
- package/dist/safe-redirect.js +2 -3
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/schema.d.ts +0 -1
- package/dist/schema.js +0 -1
- package/dist/security-schemes.d.ts +0 -1
- package/dist/security-schemes.js +0 -1
- package/dist/security.d.ts +0 -16
- package/dist/security.js +32 -4
- package/dist/session.d.ts +0 -1
- package/dist/session.js +0 -1
- package/dist/streaming.d.ts +0 -1
- package/dist/streaming.js +0 -1
- package/dist/subdomains.d.ts +0 -1
- package/dist/subdomains.js +0 -1
- package/dist/time-claims.d.ts +0 -1
- package/dist/time-claims.js +0 -1
- package/dist/tracing.d.ts +0 -1
- package/dist/tracing.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/websocket.d.ts +0 -1
- package/dist/websocket.js +0 -1
- package/package.json +6 -2
- package/dist/adapters/bun.d.ts.map +0 -1
- package/dist/adapters/bun.js.map +0 -1
- package/dist/adapters/cloudflare.d.ts.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/deno.d.ts.map +0 -1
- package/dist/adapters/deno.js.map +0 -1
- package/dist/adapters/fastly.d.ts.map +0 -1
- package/dist/adapters/fastly.js.map +0 -1
- package/dist/adapters/lambda.d.ts.map +0 -1
- package/dist/adapters/lambda.js.map +0 -1
- package/dist/adapters/node.d.ts.map +0 -1
- package/dist/adapters/node.js.map +0 -1
- package/dist/adapters/vercel.d.ts.map +0 -1
- package/dist/adapters/vercel.js.map +0 -1
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/compression.d.ts.map +0 -1
- package/dist/compression.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/conn-info.d.ts.map +0 -1
- package/dist/conn-info.js.map +0 -1
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js.map +0 -1
- package/dist/cookie.d.ts.map +0 -1
- package/dist/cookie.js.map +0 -1
- package/dist/dependency.d.ts.map +0 -1
- package/dist/dependency.js.map +0 -1
- package/dist/discriminator.d.ts.map +0 -1
- package/dist/discriminator.js.map +0 -1
- package/dist/docs.d.ts.map +0 -1
- package/dist/docs.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/etag.d.ts.map +0 -1
- package/dist/etag.js.map +0 -1
- package/dist/fetch-guard.d.ts.map +0 -1
- package/dist/fetch-guard.js.map +0 -1
- package/dist/hashing.d.ts.map +0 -1
- package/dist/hashing.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ip-restriction.d.ts.map +0 -1
- package/dist/ip-restriction.js.map +0 -1
- package/dist/jwk.d.ts.map +0 -1
- package/dist/jwk.js.map +0 -1
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js.map +0 -1
- package/dist/load-shedding.d.ts.map +0 -1
- package/dist/load-shedding.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js.map +0 -1
- package/dist/multipart.d.ts.map +0 -1
- package/dist/multipart.js.map +0 -1
- package/dist/openapi.d.ts.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/rate-limit-redis.d.ts.map +0 -1
- package/dist/rate-limit-redis.js.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js.map +0 -1
- package/dist/safe-redirect.d.ts.map +0 -1
- package/dist/safe-redirect.js.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/security-schemes.d.ts.map +0 -1
- package/dist/security-schemes.js.map +0 -1
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js.map +0 -1
- package/dist/subdomains.d.ts.map +0 -1
- package/dist/subdomains.js.map +0 -1
- package/dist/time-claims.d.ts.map +0 -1
- package/dist/time-claims.js.map +0 -1
- package/dist/tracing.d.ts.map +0 -1
- package/dist/tracing.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
package/README.md
CHANGED
|
@@ -27,12 +27,16 @@ DaloyJS exists to be the framework you'd build if you took the best ideas from e
|
|
|
27
27
|
|
|
28
28
|
| You want | Today's best-of | What DaloyJS gives you |
|
|
29
29
|
| ------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
|
-
| Best **OpenAPI ergonomics**
|
|
31
|
-
| Best **Vercel / serverless / edge fit**
|
|
32
|
-
| Mature **Swagger / docs / ops** in Node
|
|
33
|
-
| Modern **TS-first DX**, Bun acceptable
|
|
34
|
-
| Best-in-class **typed client codegen** for any consumer
|
|
35
|
-
| **
|
|
30
|
+
| Best **OpenAPI ergonomics** | [FastAPI](https://fastapi.tiangolo.com) | OpenAPI 3.1 from a single route definition; `docs: true` mounts `/docs` and `/openapi.json`. |
|
|
31
|
+
| Best **Vercel / serverless / edge fit** | [Hono](https://hono.dev/docs/) | Web-standard `Request → Response` core with adapters for Node, Bun, Deno, Cloudflare, Vercel, Fastly, and Lambda. |
|
|
32
|
+
| Mature **Swagger / docs / ops** in Node | [Fastify](https://fastify.dev/docs/latest/Reference/) | Encapsulated plugins, structured logger, graceful shutdown, request ids, and lifecycle hooks — all first-party. |
|
|
33
|
+
| Modern **TS-first DX**, Bun acceptable | [Elysia](https://elysiajs.com/at-glance.html) | End-to-end typed handlers, typed context, and a typed in-process client — no codegen step required. |
|
|
34
|
+
| Best-in-class **typed client codegen** for any consumer | [Hey API](https://heyapi.dev/openapi-ts/get-started) | One `pnpm gen` command emits a fully-typed fetch SDK from your live OpenAPI spec. |
|
|
35
|
+
| Opinionated **DI / module architecture** for large teams | [NestJS](https://docs.nestjs.com/) | Plugin encapsulation, `register()` prefixes, and `defineDependency()` typed-DI with per-request dedup — no decorators. |
|
|
36
|
+
| Minimalist **async middleware cascade** | [Koa](https://koajs.com/) | Koa-style `Context` on a web-standard core, with validation, OpenAPI, errors, and security headers in-box. |
|
|
37
|
+
| **Services + real-time** API framework | [FeathersJS](https://feathersjs.com/) | First-party `app.ws()` with CSWSH refuse-to-boot guards, plus SSE / NDJSON streaming over explicit OpenAPI routes. |
|
|
38
|
+
| Battle-tested **Node middleware compatibility** | [Express v5](https://expressjs.com/en/blog/2024-10-15-v5-release) | Regex-free trie router, schema-validated routes, RFC 9457 problem+json, and refuse-to-boot guards on every runtime. |
|
|
39
|
+
| **Portable supply-chain hardening** for the apps you build | [pnpm](https://pnpm.io/motivation) defaults + a zero-runtime-dep core | Hardened `.npmrc`, source-verified lockfiles, zero runtime deps, CycloneDX + SPDX SBOM, and npm provenance attestations. |
|
|
36
40
|
|
|
37
41
|
```
|
|
38
42
|
framework test suite passing · ≥90% line + function coverage / ≥90% branch coverage · typechecks on TypeScript 6 with `strict: true`
|
|
@@ -338,6 +342,19 @@ miss 4,763,878 ops/sec
|
|
|
338
342
|
- Body parsing is lazy and only runs when a route declares a body schema.
|
|
339
343
|
- No regex on the hot path.
|
|
340
344
|
|
|
345
|
+
### Cold-start tip (serverless / edge)
|
|
346
|
+
|
|
347
|
+
For deployments where every millisecond of startup matters (Lambda, Vercel Edge, Cloudflare Workers, Fastly Compute), import `App` from the deep entry point instead of the barrel:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { App } from "@daloyjs/core/app"; // ~13 ms faster cold start than "@daloyjs/core"
|
|
351
|
+
import { serve } from "@daloyjs/core/node";
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`@daloyjs/core/app` resolves to the **same `App` class with the same secure-by-default constructor** — `secureHeaders`, `requestId`, body limits, request timeouts, `fetchGuard`, prototype-pollution guards, problem+json redaction, and every other guardrail are still wired automatically. The deep import only skips loading unrelated peripheral modules (`jwk`, `jwt`, `multipart`, `websocket`, `streaming`, `compression`, `subdomains`, etc.) that the barrel re-exports for convenience. If you use any of those, import them directly from their own subpaths (`@daloyjs/core/jwk`, `@daloyjs/core/multipart`, …) so each one is paid for only when used.
|
|
355
|
+
|
|
356
|
+
Long-lived Node servers will not notice the difference. This is purely a cold-start optimization for serverless.
|
|
357
|
+
|
|
341
358
|
---
|
|
342
359
|
|
|
343
360
|
## Test client + contract tests
|
|
@@ -409,32 +426,131 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
|
|
|
409
426
|
|
|
410
427
|
## Status
|
|
411
428
|
|
|
412
|
-
DaloyJS is in **public preview** (`0.x`). The public API may still change between minor versions; deprecations will get at least one minor cycle once `1.0.0` ships. The framework is already in use for production trials
|
|
429
|
+
DaloyJS is in **public preview** (`0.x`). The public API may still change between minor versions; deprecations will get at least one minor cycle once `1.0.0` ships. The framework is already in use for production trials.
|
|
413
430
|
|
|
414
|
-
|
|
431
|
+
**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.
|
|
415
432
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
- Built-in security primitives (body limits, prototype-pollution-safe JSON, path-traversal guard, request timeouts, header injection guards, **duplicate `Host` / `Content-Length` rejection**, **stripped `Server` / `X-Powered-By` headers by default**, **structured-log redaction defaults** for authorization / cookie / password / token / JWT-shaped values, **`secureHeaders()` auto-applied since `0.16.0`** with user-installed overrides automatically replacing the auto instance, **cross-origin state-changing requests rejected with `403` since `0.16.0`** unless a route's `cors()` policy allows the request origin, **refuse-to-boot on weak session secrets, `cors({ origin: "*" })`, `session()` + state-changing route without `csrf()`, and unconfigured `X-Forwarded-*` in production since `0.17.0`**, **connection-draining shutdown with `Connection: close` on `503` and in-flight responses, `crashOnUnhandledRejection` default-on in production, and `app.healthcheck()` / `app.readinesscheck()` primitives with bearer-token auth + per-IP rate limit since `0.18.0`**, **`rateLimit({ groupId })` shared buckets, `combine` primitives `every` / `some` / `except`, `ipRestriction()` with CIDR-aware IPv4/IPv6 allow/deny lists, and `internal: true` route flag + `app.inject()` since `0.19.0`**, **`loadShedding()` event-loop-pressure middleware (auto-`503` + `Retry-After`), `app.cspReportRoute()` rate-limited CSP violation receiver + `secureHeaders({ reportingEndpoints, reportTo })` wiring, `disconnectStatusCode: 499` default for client-aborted requests, and `defineConfig({ schema, source })` boot-time typed configuration validation since `0.20.0`**, **`createJwtSigner()` / `createJwtVerifier()` with `alg`-discipline (no `alg: "none"`, explicit allowlist, HS+JWK refused at construction) + `exp`-required sign refusal, `requireScopes()` with RFC-6750 `WWW-Authenticate: Bearer` challenge + per-request scope aggregation, and `etag()` helper with `Set-Cookie` / `Cache-Control: private | no-store | no-cache` auto-skip (cross-tenant fingerprinting defense) since `0.21.0`**, **`jwk()` asymmetric-only JWKS middleware (refuses `HS*` at construction, `kid` + JWT-vs-JWK `alg` cross-check, `https://` JWKS URL with TTL caching + in-flight-promise dedup, normalizes `scope` / `scp` / `scopes` claims), per-scheme `verify(credentials, ctx)` revalidation hook on `bearerAuth()` / `jwk()`, `basicAuth({ onAuthSuccess })` typed-context callback, and `Cache-Control: no-store` on every first-party auth helper 401 challenge since `0.22.0`**, **`wsRateLimit()` for WebSocket upgrades, `loginThrottle()` credential-entry preset, `rotateSession()` privilege-change session rotation, `fileField({ magicBytes })` upload signature checks, `requirePayloadAuth` security-scheme guard, and WebSocket safe defaults since `0.23.0`**, **`app({ behindProxy })` declarative model (replaces `trustProxy`), adapter-independent `ConnInfo` abstraction (`getConnInfo()` / lazy `ctx.remoteAddress` / `ctx.remotePort`), `daloy doctor` production-posture validator (with `--audit-secrets` + `--no-audit-defaults`), container-first `create-daloy` templates (`HEALTHCHECK` to `/readyz`, `STOPSIGNAL SIGTERM`, non-root user, `tini` PID 1), PSL-aware `subdomains()` helper with `≤ 90 days` snapshot guard, plugin `dependencies: string[]` refuse-to-boot, namespace-protected `decorate({ override })`, plugin extension ordering with `before` / `after` + cycle detection, `behindProxy.hops` collapses to the `(N+1)`-from-rightmost slot, `defineDependency()` typed-DI helper with per-request deduplication, scheme-aware `ctx.state.auth` typed contract, plugin lifecycle encapsulation default of `local`, and required `name` + optional `seed` for stateful plugins since `0.24.0`**, **`compression()` middleware (built on the web-standard `CompressionStream`, prefers `br` > `gzip` > `deflate` and probes runtime support once) with BREACH-aware always-on guards (skip `Set-Cookie` / `Authorization` / session-or-CSRF cookie / 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 since `0.25.0`**, **`secureDefaults: false` refuse-to-construct in production (unless `acknowledgeInsecureDefaults: true`) + once-per-process `error` log naming every disabled default, `createJwtSigner()` / `createJwtVerifier()` refuse HS-shaped secrets `< 32` bytes (RFC 7518 §3.2), `secureHeaders()` refuses to construct with both `frameOptions: false` AND no CSP `frame-ancestors` directive (no clickjacking defense), and mandatory hardware-backed 2FA for every contributor with publish access (documented in `SECURITY.md`) since `0.26.0`**, and **single-source-of-truth cookie and temporal-claim helpers (`@daloyjs/core/cookie`, `@daloyjs/core/time-claims`), `session()` / `csrf()` `__Secure-` cookie refuse-to-boot, zero-runtime-dependency CI governance, and secret-comparison CI grep gate since `0.27.0`**, and **pattern-agnostic-framework parity audit suite since `0.28.0`** (`scripts/verify-parity-audits.ts` static gates wired into CI as `pnpm verify:parity-audits` — refuses public setters on `request.url` / `request.path` / `request.method`, refuses `ctx.respond = false`-style response-bypass switches, refuses `Referer`-based redirect targets, refuses `AES-CBC` / `SHA-1` / third-party crypto reach inside the cookie helper module, refuses adapter dispatch via `allowInternal: true` / `app.inject()`, and reaffirms the zero-runtime-dependency posture — plus `daloy doctor --audit-defaults` live-config checks that flag wildcard-credentials CORS, > 24 h CORS `maxAge`, > 25 MiB blanket body limits, zero `idleTimeoutMs` in production, `allowUnsafeValidationDetails` / `exposeFrameworkIdentity` / `enableServerTimingInProduction` opt-ins that the public type does not expose), and **zero-runtime-dependency batteries-included parity & governance audit suite since `0.29.0`** (`SECURITY-CONTACTS.md` rotation file with machine-readable ACTIVE block + `<!-- last-exercise: -->` marker, `scripts/verify-governance-audits.ts` static gates wired into CI as `pnpm verify:governance-audits` — refuses a missing/stale rotation file, refuses runtime deps on `@daloyjs/core/package.json`, refuses removal of the plugin-prerequisite refuse-to-boot path or the `topoSortExtensions` cycle-detection throw from `src/app.ts`, and reaffirms the governance floor: top-level `permissions:` on every workflow, `persist-credentials: false` on every `actions/checkout`, 40-hex SHA pinning on every third-party `uses:`, `step-security/harden-runner` on every workflow that uses third-party actions, and `.github/CODEOWNERS` on privileged files — plus a release-workflow contributor-rotation refusal step that exits non-zero when `github.actor` is not on the `SECURITY-CONTACTS` ACTIVE rotation), and **multi-runtime web-standard ergonomic-framework parity bake-ins since `0.30.0`** (`Cache-Control: no-store` baked into `UnauthorizedError` / `ForbiddenError` / `TooManyRequestsError` so every first-party auth helper 401 / 403 / 429 response is uncacheable, `cspReportRoute()` refuses `application/json` with `415` and refuses `maxBodyBytes > 64 KiB` at construction with the default production logger sink omitting the parsed report body unless `logCspReportBodies: true` is set explicitly (PII defense — CSP reports include the violated source URL), `cors()` `allowMethods` default narrowed to `[GET, HEAD, POST]` with `methods: ['*']` refused at construction (`PUT` / `PATCH` / `DELETE` are now explicit opt-ins), and `scripts/verify-runtime-parity-audits.ts` wired into CI as `pnpm verify:runtime-parity-audits` covering all of the above plus the reverse-proxy-helper absence audit and the compression skip-already-encoded reaffirm), and **mature-Node ergonomic-framework second-pass bake-ins since `0.31.0`** (`useSemicolonDelimiter: false` router-level audit so `/users/42;admin=true` cannot smuggle attacker-controlled query data past auth / CSRF / rate-limit middleware via a reverse-proxy / origin disagreement on RFC 3986 path-segment delimiters, `allowErrorHandlerOverride: false` audit so the framework never ships a standalone `setErrorHandler()` / `onError()` class method that could silently overwrite previously-registered error handlers, `requestId()` `trustIncoming: false` default audit so client-supplied `X-Request-ID` headers cannot poison framework logs by default, RFC 7231 + RFC 5789 HTTP-method allowlist now runtime-enforced inside `app.route()` (WebDAV / `TRACE` / `CONNECT` rejected at the framework boundary), `Connection: close` on every response produced during graceful shutdown reaffirmed by audit, and `scripts/verify-routing-hardening-audits.ts` wired into CI as `pnpm verify:routing-hardening-audits` covering all of the above), and **leftover focused slice since `0.32.0`** (`app.ws()` now scans the effective hook stack for header-mutating middleware — `secureHeaders()` / `cors()` / `csrf()` / `compression()` — and refuses-at-registration with a structured error naming both the WebSocket route AND every conflicting middleware unless the handler opts in via `acknowledgeHeaderMutatingMiddleware: true`, new `httpError({ status, problem, headers?, res? })` factory in `@daloyjs/core` extracts headers from a custom `Response` and refuses-at-construction with `MessageLeakError` when the response would leak request-scoped state — `Set-Cookie`, `Server-Timing`, `X-*-Token`, or `Cache-Control` other than `no-store` / `no-cache` all trip the gate, leaving only the `WWW-Authenticate` / `Proxy-Authenticate` / `Retry-After` / `Content-Type` / `Content-Language` allowlist (with `Content-Length` accepted for safety validation but not forwarded), plus a new `ProblemRenderOptions.contextHeaders` extension so direct callers of `HttpError.toResponse()` get the same Context-merge as the framework boundary, and the `PluginExtension` contract now accepts `responseHeaders?: readonly string[]` — `topoSortExtensions()` refuses-at-call when two extensions declare overlapping `responseHeaders` without declaring a `before` / `after` relationship between them so plugin-registration order can no longer non-deterministically pick a winner on a shared response header) — WebSocket public-route exposure can be acknowledged explicitly, while middleware/header-order checks remain registration invariants) plus first-party middleware (`secureHeaders` with CSP nonce + Trusted Types, `cors`, `rateLimit`, `requestId`, `bearerAuth`, `basicAuth`, `csrf` with **double-submit cookie** + **Fetch-Metadata** strategies, `session`, `timing` / `timingSafeEqual`) and **zero-knob crypto helpers** (`passwordHash` / `passwordVerify` at `@daloyjs/core/hashing`, `verifyWebhookSignature` / `signWebhookPayload`).
|
|
419
|
-
-
|
|
420
|
-
-
|
|
421
|
-
-
|
|
422
|
-
-
|
|
423
|
-
- Registration-conflict guards are deliberate startup invariants: use `acknowledgeHeaderMutatingMiddleware` for reviewed WebSocket/middleware scopes and explicit plugin `before` / `after` ordering for shared `responseHeaders`, rather than expecting `secureDefaults: false` to suppress those checks.
|
|
424
|
-
- Pretty `printStartupBanner()` / `formatStartupBanner()` startup helpers at `@daloyjs/core/banner`, used by every starter template so `pnpm dev` greets you with a colorized boxed panel (TTY + `NO_COLOR` / `FORCE_COLOR` aware, with an ASCII fallback for dumb terminals).
|
|
433
|
+
### Routing, validation, and docs
|
|
434
|
+
|
|
435
|
+
- Contract-first routing with Standard Schema validation (Zod 4, Valibot, ArkType, TypeBox) and OpenAPI 3.1 generated from a single source of truth.
|
|
436
|
+
- Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true`, with Scalar UI theming and custom CSS via `docs.scalar`.
|
|
437
|
+
- Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno); explicit `openapi.info` values always win.
|
|
438
|
+
- RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
|
|
439
|
+
- 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`.
|
|
425
440
|
- In-process test client (`app.request()`), contract-test runner, in-process typed client, and Hey API codegen via `pnpm gen`.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
-
|
|
430
|
-
- `
|
|
431
|
-
-
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
|
|
442
|
+
### Runtimes and deployment
|
|
443
|
+
|
|
444
|
+
- Adapters for Node (Heroku, Railway, Render, Fly.io), Bun, Deno, Cloudflare Workers, Vercel Node / Edge / Next.js / Netlify Edge, Fastly Compute, and AWS Lambda / Netlify Functions / Lambda Function URLs.
|
|
445
|
+
- `daloy dev` watch loop delegates to the host runtime's native watcher (`node --import tsx --watch`, `bun --hot`, or `deno run --watch`) with a `--runtime` override for cross-runtime `package.json` scripts.
|
|
446
|
+
- `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel Edge templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening.
|
|
447
|
+
- Container-first templates: `HEALTHCHECK` to `/readyz`, `STOPSIGNAL SIGTERM`, non-root user, `tini` as PID 1.
|
|
448
|
+
- Generated `deploy.yml` for container templates signs every pushed GHCR image with **Sigstore Cosign** (keyless OIDC) and attaches an **SPDX SBOM attestation** so consumers can `cosign verify` and `cosign verify-attestation --type spdxjson` instead of trusting the registry alone.
|
|
449
|
+
- Pretty `printStartupBanner()` / `formatStartupBanner()` helpers at `@daloyjs/core/banner`, used by every starter template (TTY + `NO_COLOR` / `FORCE_COLOR` aware, ASCII fallback for dumb terminals).
|
|
450
|
+
|
|
451
|
+
### Core security primitives
|
|
452
|
+
|
|
453
|
+
- Body limits, prototype-pollution-safe JSON, path-traversal guard, request timeouts, header injection guards.
|
|
454
|
+
- Request-smuggling defense: duplicate `Host`, `Content-Length`, and `Transfer-Encoding` headers are rejected.
|
|
455
|
+
- `Server` and `X-Powered-By` headers stripped by default.
|
|
456
|
+
- Structured-log redaction defaults for authorization, cookie, password, token, and JWT-shaped values.
|
|
457
|
+
- `secureHeaders()` auto-applied; user-installed instances automatically replace the auto one.
|
|
458
|
+
- Cross-origin state-changing requests rejected with `403` unless a route's `cors()` policy allows the origin.
|
|
459
|
+
- Production mode strips `detail` from 5xx problem+json automatically.
|
|
460
|
+
- Real **405** with `Allow` header instead of a misleading 404.
|
|
461
|
+
- `Cache-Control: no-store` baked into `UnauthorizedError` / `ForbiddenError` / `TooManyRequestsError` so every first-party auth 401 / 403 / 429 response is uncacheable.
|
|
462
|
+
|
|
463
|
+
### Refuse-to-boot guardrails
|
|
464
|
+
|
|
465
|
+
The framework refuses to start (or to construct) when configuration is unsafe:
|
|
466
|
+
|
|
467
|
+
- Weak session secrets, `cors({ origin: "*" })` with credentials, `session()` + state-changing route without `csrf()`, and unconfigured `X-Forwarded-*` in production.
|
|
468
|
+
- `secureDefaults: false` in production unless `acknowledgeInsecureDefaults: true` is set, plus a once-per-process `error` log naming every disabled default.
|
|
469
|
+
- `createJwtSigner()` / `createJwtVerifier()` refuse `alg: "none"`, accept only an explicit allowlist, refuse HS + JWK combinations, refuse to sign without `exp`, and refuse HS-shaped secrets under 32 bytes (RFC 7518 §3.2).
|
|
470
|
+
- `secureHeaders()` refuses to construct with `frameOptions: false` AND no CSP `frame-ancestors` directive (no clickjacking defense).
|
|
471
|
+
- `cors()` refuses `methods: ['*']` at construction; default `allowMethods` narrowed to `[GET, HEAD, POST]` so `PUT` / `PATCH` / `DELETE` become explicit opt-ins.
|
|
472
|
+
- `cspReportRoute()` refuses non-`application/json` (415) and refuses `maxBodyBytes > 64 KiB` at construction. The default production logger sink omits the parsed report body unless `logCspReportBodies: true` is set explicitly.
|
|
473
|
+
- `session()` and `csrf()` refuse cookies that violate the `__Secure-` prefix policy.
|
|
474
|
+
- Plugin `dependencies: string[]` refuse-to-boot when a prerequisite is missing; `topoSortExtensions()` refuses cycles, and refuses two extensions declaring overlapping `responseHeaders` without a `before` / `after` relationship.
|
|
475
|
+
- `app.ws()` scans the effective hook stack and refuses-at-registration when header-mutating middleware (`secureHeaders()`, `cors()`, `csrf()`, `compression()`) is present, unless the handler opts in via `acknowledgeHeaderMutatingMiddleware: true`.
|
|
476
|
+
|
|
477
|
+
### First-party middleware
|
|
478
|
+
|
|
479
|
+
- `secureHeaders` with strict CSP baseline, per-request **nonces**, **Trusted Types** (`require-trusted-types-for 'script'`), `frame-ancestors`, `cross-origin-opener-policy` / `cross-origin-resource-policy`, and reporting endpoints.
|
|
480
|
+
- `cors` with explicit-allowlist enforcement.
|
|
481
|
+
- `csrf` with **double-submit cookie** (default) and **Fetch-Metadata** (`Sec-Fetch-Site`-based, tokenless) strategies; timing-safe verification.
|
|
482
|
+
- `rateLimit` with token-bucket + `Retry-After`, shared `groupId` buckets, and a Redis-backed store at `@daloyjs/core/rate-limit-redis`.
|
|
483
|
+
- `loadShedding()` event-loop-pressure middleware (auto-`503` + `Retry-After`).
|
|
484
|
+
- `loginThrottle()` credential-entry preset and `rotateSession()` privilege-change session rotation.
|
|
485
|
+
- `ipRestriction()` with CIDR-aware IPv4 / IPv6 allow / deny lists.
|
|
486
|
+
- `combine` primitives: `every`, `some`, `except`.
|
|
487
|
+
- `requestId()` with cryptographic ids; `trustIncoming: false` by default so client-supplied `X-Request-ID` headers cannot poison logs.
|
|
488
|
+
- `bearerAuth()` and `basicAuth()` with per-scheme `verify(credentials, ctx)` revalidation hooks, typed-context `onAuthSuccess` callback, and `Cache-Control: no-store` on every 401 challenge.
|
|
489
|
+
- `jwk()` asymmetric-only JWKS middleware: refuses `HS*` at construction, cross-checks `kid` and JWT-vs-JWK `alg`, requires `https://` JWKS URLs with TTL caching + in-flight-promise dedup, normalizes `scope` / `scp` / `scopes` claims.
|
|
490
|
+
- `requireScopes()` with RFC-6750 `WWW-Authenticate: Bearer` challenge and per-request scope aggregation.
|
|
491
|
+
- `session()` with signed cookies and pluggable stores.
|
|
492
|
+
- `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.
|
|
493
|
+
- `etag()` helper auto-skips on `Set-Cookie` and private / no-store / no-cache `Cache-Control` (cross-tenant fingerprinting defense).
|
|
494
|
+
- `timing` / `timingSafeEqual` helpers.
|
|
495
|
+
- `fileField({ magicBytes })` upload signature checks.
|
|
496
|
+
- `ipRestriction()`, `wsRateLimit()`, `requirePayloadAuth` security-scheme guard.
|
|
497
|
+
- Zero-knob crypto helpers: `passwordHash` / `passwordVerify` at `@daloyjs/core/hashing`, `verifyWebhookSignature` / `signWebhookPayload`.
|
|
498
|
+
- `fetchGuard()` SSRF defaults.
|
|
499
|
+
|
|
500
|
+
### WebSockets
|
|
501
|
+
|
|
502
|
+
- WebSocket primitives with the Bun-style handler shape (`open` / `message` / `close` / `drain` / `error`) running on both Node and Bun adapters.
|
|
503
|
+
- Typed `app.ws(path, handler)` registration; the upgrade listener is only installed when WS routes exist.
|
|
504
|
+
- Production WebSocket routes under `secureDefaults` require:
|
|
505
|
+
- a pre-upgrade `beforeUpgrade` decision hook or an explicit `acknowledgeUnauthenticated: true`, **AND**
|
|
506
|
+
- an Origin policy (`allowedOrigins: "same-origin"` / `string[]` / predicate) or `acknowledgeCrossOriginUpgrade: true`.
|
|
507
|
+
|
|
508
|
+
This closes the Cross-Site WebSocket Hijacking (CSWSH) class of bug — Storybook's [CVE-2026-27148](https://www.aikido.dev/blog/storybooks-websockets-attack) is the representative case: cookie auth alone does not stop a malicious site from opening an authenticated WS handshake from a victim's browser. The Origin check runs **before** `beforeUpgrade` in both adapters.
|
|
509
|
+
|
|
510
|
+
### Lifecycle and ops
|
|
511
|
+
|
|
512
|
+
- Plugin encapsulation (Fastify-style), decorators, structured logging, request-id propagation.
|
|
513
|
+
- Lifecycle events: `onPluginInstalled`, `onShutdown`, `onClose`.
|
|
514
|
+
- Connection-draining graceful shutdown with `Connection: close` on `503` and in-flight responses.
|
|
515
|
+
- `crashOnUnhandledRejection` default-on in production.
|
|
516
|
+
- `app.healthcheck()` / `app.readinesscheck()` primitives with bearer-token auth and per-IP rate limit.
|
|
517
|
+
- `disconnectStatusCode: 499` default for client-aborted requests.
|
|
518
|
+
- `defineConfig({ schema, source })` boot-time typed configuration validation.
|
|
519
|
+
- `app({ behindProxy })` declarative model (replaces `trustProxy`); `behindProxy.hops` collapses to the `(N+1)`-from-rightmost slot.
|
|
520
|
+
- Adapter-independent `ConnInfo` abstraction: `getConnInfo()`, lazy `ctx.remoteAddress`, `ctx.remotePort`.
|
|
521
|
+
- `daloy doctor` production-posture validator with `--audit-secrets` and `--audit-defaults` (flags wildcard-credentials CORS, > 24h CORS `maxAge`, > 25 MiB blanket body limits, zero `idleTimeoutMs` in production, and unsafe opt-ins).
|
|
522
|
+
- PSL-aware `subdomains()` helper with a `≤ 90 days` snapshot guard.
|
|
523
|
+
- `defineDependency()` typed-DI helper with per-request deduplication.
|
|
524
|
+
- Scheme-aware `ctx.state.auth` typed contract; named, optionally seeded stateful plugins.
|
|
525
|
+
|
|
526
|
+
### Streaming and integrations
|
|
527
|
+
|
|
528
|
+
- Streaming helpers (SSE + NDJSON), multipart ergonomics, OpenTelemetry-compatible tracing.
|
|
529
|
+
- Integration guides for transactional email — AWS SES, SendGrid, Resend, Postmark, Mailgun, Mailtrap — with a common `EmailSender` plugin pattern and runtime-compatibility matrix.
|
|
530
|
+
- Authentication & authorization guides for AWS Cognito, Microsoft Entra ID (MSAL), Auth0, Okta, and Clerk — with a common bearer-auth plugin, scope / role enforcement, and runtime-compatibility matrix.
|
|
531
|
+
|
|
532
|
+
### Supply-chain hardening (CI)
|
|
533
|
+
|
|
534
|
+
A growing suite of static gates runs on every push and PR:
|
|
535
|
+
|
|
536
|
+
- Parity / governance / runtime-parity / routing-hardening audits: `verify:parity-audits`, `verify:governance-audits`, `verify:runtime-parity-audits`, `verify:routing-hardening-audits`.
|
|
537
|
+
- 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`.
|
|
538
|
+
- Agent-skill gates: `verify:no-leaky-agent-skills`, `verify:no-toxic-agent-skills`, `verify:no-toxic-skills`.
|
|
539
|
+
- 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).
|
|
540
|
+
- 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, and npm-package-aliasing dependency-confusion patterns.
|
|
541
|
+
- `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.
|
|
542
|
+
- Governance floor reaffirmed by audit: top-level `permissions:` on every workflow, `persist-credentials: false` on every `actions/checkout`, 40-hex SHA pinning on every third-party `uses:`, `step-security/harden-runner` on every workflow using third-party actions, and `.github/CODEOWNERS` on privileged files.
|
|
543
|
+
- Mandatory hardware-backed 2FA for every contributor with publish access (documented in `SECURITY.md`).
|
|
544
|
+
- `@daloyjs/core` is published with CycloneDX 1.5 + SPDX 2.3 SBOMs and npm `--provenance`; the release workflow uses `npm stage publish` so the protected `npm-publish` GitHub Environment approval is followed by an out-of-band `npm stage approve` step with maintainer MFA before any version is installable.
|
|
545
|
+
|
|
546
|
+
### Other helpers
|
|
547
|
+
|
|
548
|
+
- Single-source-of-truth cookie and temporal-claim helpers at `@daloyjs/core/cookie` and `@daloyjs/core/time-claims`.
|
|
549
|
+
- `httpError({ status, problem, headers?, res? })` factory extracts headers from a custom `Response` and refuses-at-construction with `MessageLeakError` if the response would leak request-scoped state (`Set-Cookie`, `Server-Timing`, `X-*-Token`, or any `Cache-Control` other than `no-store` / `no-cache`). The allowlist is `WWW-Authenticate` / `Proxy-Authenticate` / `Retry-After` / `Content-Type` / `Content-Language` (with `Content-Length` accepted for safety validation but not forwarded).
|
|
550
|
+
- `ProblemRenderOptions.contextHeaders` lets direct callers of `HttpError.toResponse()` get the same Context-merge as the framework boundary.
|
|
551
|
+
- A self-paced [workshop](./workshop/README.md) (4-hour and 8-hour tracks) for senior TypeScript / Node developers: contract-first routes, validation, errors, middleware composition, JWT / JWK, sessions, WebSocket upgrades, CSRF / CORS, `fetchGuard()` SSRF defaults, OpenAPI tuning, and contract testing. Every exercise is a single self-contained `tsx --watch` file with ordered coding steps and reference solutions.
|
|
552
|
+
|
|
553
|
+
Roadmap and shipped / in-progress checklists live in [ROADMAP.md](./ROADMAP.md).
|
|
438
554
|
|
|
439
555
|
## Contributing
|
|
440
556
|
|
package/dist/adapters/bun.d.ts
CHANGED
package/dist/adapters/bun.js
CHANGED
|
@@ -27,4 +27,3 @@ interface ExecutionContextLike {
|
|
|
27
27
|
/** Wrap an {@link App} in the `{ fetch }` object expected by Cloudflare Workers and other web-standard hosts. */
|
|
28
28
|
export declare function toFetchHandler<Env = unknown>(app: App): ExportedFetchHandler<Env>;
|
|
29
29
|
export {};
|
|
30
|
-
//# sourceMappingURL=cloudflare.d.ts.map
|
package/dist/adapters/deno.d.ts
CHANGED
package/dist/adapters/deno.js
CHANGED
|
@@ -22,4 +22,3 @@ import type { App } from "../app.js";
|
|
|
22
22
|
export declare function toFastlyHandler(app: App): (req: Request) => Promise<Response>;
|
|
23
23
|
/** Register a Fastly Compute `fetch` event listener that delegates to the given {@link App}. */
|
|
24
24
|
export declare function installFastlyListener(app: App): void;
|
|
25
|
-
//# sourceMappingURL=fastly.d.ts.map
|
package/dist/adapters/fastly.js
CHANGED
|
@@ -71,4 +71,3 @@ export type LambdaResponse = LambdaResponseV1 | LambdaResponseV2;
|
|
|
71
71
|
export type LambdaHandler = (event: LambdaEvent) => Promise<LambdaResponse>;
|
|
72
72
|
/** Wrap an {@link App} as a Lambda/Netlify handler accepting either v1.0 or v2.0 event payloads. */
|
|
73
73
|
export declare function toLambdaHandler(app: App): LambdaHandler;
|
|
74
|
-
//# sourceMappingURL=lambda.d.ts.map
|
package/dist/adapters/lambda.js
CHANGED
package/dist/adapters/node.d.ts
CHANGED
|
@@ -23,6 +23,17 @@ export interface NodeServerOptions {
|
|
|
23
23
|
* clients can spoof the scheme/host. Default: false.
|
|
24
24
|
*/
|
|
25
25
|
trustProxy?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum declared `Content-Length` (in bytes) for which the Node adapter
|
|
28
|
+
* pre-buffers the request body into a `Uint8Array` before constructing the
|
|
29
|
+
* `Request`. Bodies above this threshold fall back to the streaming
|
|
30
|
+
* `Readable.toWeb(req)` path so the adapter never holds an unbounded buffer
|
|
31
|
+
* per in-flight request — important under high concurrency where N
|
|
32
|
+
* simultaneous large uploads would otherwise pin N × threshold bytes of
|
|
33
|
+
* memory. The threshold is independently capped by `App.bodyLimitBytes`,
|
|
34
|
+
* which is the actual security limit. Default: 256 KiB.
|
|
35
|
+
*/
|
|
36
|
+
bufferedBodyMaxBytes?: number;
|
|
26
37
|
}
|
|
27
38
|
/** Handle returned by {@link serve} exposing the underlying Node `Server` plus a `close()` for graceful shutdown. */
|
|
28
39
|
export interface NodeServerHandle {
|
|
@@ -32,4 +43,3 @@ export interface NodeServerHandle {
|
|
|
32
43
|
}
|
|
33
44
|
/** Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}. */
|
|
34
45
|
export declare function serve(app: App, opts?: NodeServerOptions): NodeServerHandle;
|
|
35
|
-
//# sourceMappingURL=node.d.ts.map
|