@daloyjs/core 0.34.3 → 0.35.1
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 +151 -32
- 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 +0 -1
- package/dist/adapters/node.js +175 -35
- package/dist/adapters/vercel.d.ts +0 -1
- package/dist/adapters/vercel.js +0 -1
- package/dist/app.d.ts +18 -1
- package/dist/app.js +396 -118
- package/dist/banner.d.ts +0 -1
- package/dist/banner.js +0 -1
- package/dist/cli.d.ts +32 -1
- package/dist/cli.js +62 -5
- 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 +0 -1
- package/dist/etag.d.ts +0 -1
- package/dist/etag.js +0 -1
- package/dist/fetch-guard.d.ts +50 -6
- package/dist/fetch-guard.js +83 -18
- package/dist/hashing.d.ts +0 -1
- package/dist/hashing.js +0 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- 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 +15 -1
- package/dist/jwt.js +56 -3
- 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 +78 -1
- package/dist/middleware.js +150 -18
- 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 +91 -0
- package/dist/safe-redirect.js +168 -0
- package/dist/sbom.cdx.json +13 -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 +134 -3
- package/dist/security.js +265 -6
- 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 +35 -15
- 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/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`
|
|
@@ -83,7 +87,7 @@ See [Scaffold a project](https://daloyjs.dev/docs/scaffolder) for templates and
|
|
|
83
87
|
|
|
84
88
|
## Install core manually
|
|
85
89
|
|
|
86
|
-
DaloyJS is distributed via **pnpm** for [supply-chain hygiene](https://pnpm.io/motivation) and backed by a hardened release pipeline — strict isolation, content-addressable store, deterministic lockfile, no phantom dependencies, SHA-pinned CI actions, and provenance
|
|
90
|
+
DaloyJS is distributed via **pnpm** for [supply-chain hygiene](https://pnpm.io/motivation) and backed by a hardened release pipeline — strict isolation, content-addressable store, deterministic lockfile, no phantom dependencies, SHA-pinned CI actions, npm staged publishing, and provenance attestations.
|
|
87
91
|
|
|
88
92
|
```bash
|
|
89
93
|
pnpm add @daloyjs/core zod@^4
|
|
@@ -102,9 +106,8 @@ verify-store-integrity=true
|
|
|
102
106
|
provenance=true
|
|
103
107
|
```
|
|
104
108
|
|
|
105
|
-
These defaults block transitive lifecycle scripts, wait 24 hours before resolving freshly published versions, verify the pnpm store, and require provenance on publish. The few dependencies that truly need install-time builds are allowlisted in `
|
|
106
|
-
|
|
107
|
-
The same defaults blunt **[slopsquatting](https://www.aikido.dev/blog/slopsquatting-ai-package-hallucination-attacks)** — the supply-chain attack where an AI coding assistant hallucinates a package name (`request-promise-native2`, `@types/fastify-helmet`, etc.) and an attacker registers it on npm with a malicious payload. `minimum-release-age=1440` refuses to install anything published in the last 24 hours (the typical detect-and-unpublish window), `ignore-scripts=true` suppresses lifecycle payloads, `blockExoticSubdeps: true` and `pnpm verify:lockfile` reject non-registry sources, and `@daloyjs/core`'s zero-runtime-dep posture means a hallucinated dep cannot transitively land in the published tarball. See [SECURITY.md § Slopsquatting](SECURITY.md#slopsquatting--ai-package-hallucination-aikido-2025-pattern) for the full mapping.
|
|
109
|
+
These defaults block transitive lifecycle scripts, wait 24 hours before resolving freshly published versions, verify the pnpm store, and require provenance on publish. The few dependencies that truly need install-time builds are allowlisted in [`pnpm-workspace.yaml`](pnpm-workspace.yaml) under `allowBuilds` (currently `esbuild` only), and CI runs `pnpm verify:lockfile` to reject git dependency sources and non-registry tarball URLs in `pnpm-lock.yaml`.
|
|
110
|
+
The same defaults blunt **[slopsquatting](https://www.aikido.dev/blog/slopsquatting-ai-package-hallucination-attacks)** — the supply-chain attack where an AI coding assistant hallucinates a package name (`request-promise-native2`, `@types/fastify-helmet`, etc.) and an attacker registers it on npm with a malicious payload. `minimum-release-age=1440` refuses to install anything published in the last 24 hours (the typical detect-and-unpublish window), `ignore-scripts=true` suppresses lifecycle payloads, `blockExoticSubdeps: true` and `pnpm verify:lockfile` reject non-registry sources, `pnpm verify:known-dep-names` ([`scripts/verify-known-dep-names.ts`](scripts/verify-known-dep-names.ts)) refuses any top-level dep name across the workspace that is not on an explicit allowlist (so `pnpm add <hallucinated-name>` cannot land in any `package.json` without a one-line diff that forces a name-review checkpoint), and `@daloyjs/core`'s zero-runtime-dep posture means a hallucinated dep cannot transitively land in the published tarball. See [SECURITY.md § Slopsquatting](SECURITY.md#slopsquatting--ai-package-hallucination-aikido-2025-pattern) for the full mapping.
|
|
108
111
|
|
|
109
112
|
Run `pnpm audit --prod` regularly (or `pnpm run audit` in this repo) — and `pnpm install --frozen-lockfile --ignore-scripts` in CI.
|
|
110
113
|
|
|
@@ -126,9 +129,10 @@ pnpm verify:sbom
|
|
|
126
129
|
You do **not** need to remember to run those commands manually for CI or publish:
|
|
127
130
|
|
|
128
131
|
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs `pnpm gen:sbom` and `pnpm verify:sbom` on every push to `main` and every PR.
|
|
129
|
-
- [`.github/workflows/release.yml`](.github/workflows/release.yml) reruns `pnpm gen:sbom` and `pnpm verify:sbom` before either npm publish job is allowed to proceed.
|
|
132
|
+
- [`.github/workflows/release.yml`](.github/workflows/release.yml) reruns `pnpm gen:sbom` and `pnpm verify:sbom` before either npm staged-publish job is allowed to proceed.
|
|
130
133
|
|
|
131
134
|
That means a release will fail before publish if the SBOMs are missing, stale, or inconsistent with `package.json`.
|
|
135
|
+
The workflow stages releases on npm instead of making them installable immediately, so a maintainer still has to review the stage ID and approve it with npm MFA.
|
|
132
136
|
|
|
133
137
|
For maintainers, the safe rule is: use one publish path per version. Either publish through the protected GitHub release workflow, or publish locally for an exceptional case, but do not do both for the same version.
|
|
134
138
|
|
|
@@ -318,9 +322,9 @@ deployment.
|
|
|
318
322
|
| **Method confusion** | Real **405** with `Allow` header, not a misleading 404. |
|
|
319
323
|
| **CORS misconfig** | First-party `cors()` requires an explicit allowlist and throws for `*` with credentials. |
|
|
320
324
|
| **Request correlation** | First-party `requestId()` uses cryptographic ids; scaffolded apps enable it. |
|
|
321
|
-
| **Supply chain (portable)** | pnpm `ignore-scripts=true`, `minimum-release-age=1440`, verified store, reproducible lockfile, `pnpm verify:lockfile` source verification
|
|
325
|
+
| **Supply chain (portable)** | pnpm scaffolds keep `ignore-scripts=true`, `minimum-release-age=1440`, verified store, reproducible lockfile, and `pnpm verify:lockfile` source verification; every app also installs a zero-runtime-dependency `@daloyjs/core` published with CycloneDX + SPDX SBOM and npm provenance you can verify on install — regardless of where you host your repo. |
|
|
322
326
|
|
|
323
|
-
**Portable vs. GitHub-only.** The protections
|
|
327
|
+
**Portable vs. GitHub-only.** The runtime protections and the published `@daloyjs/core` SBOM/provenance travel with every app you scaffold, no matter which CI host you use — GitLab, Bitbucket, Azure DevOps, Jenkins, on-prem, or laptop. The strongest install-time bundle is available when you choose pnpm, because `minimum-release-age`, `blockExoticSubdeps`, and the workspace gates are pnpm features. The **`@daloyjs/core` release pipeline itself** is separately hardened on GitHub Actions — no `pull_request_target`, no Actions cache, top-level `permissions: {}`, `step-security/harden-runner`, a protected `release.yml`, npm trusted publishing with `--provenance`, CodeQL + Opengrep dual SAST, OpenSSF Scorecard, zizmor, Dependabot, and CODEOWNERS — and `create-daloy --with-ci` ships the app-safe parts as an **optional GitHub Actions bundle** for teams on GitHub. See [SECURITY.md](SECURITY.md) and the [supply-chain security docs](https://daloyjs.dev/docs/security/supply-chain).
|
|
324
328
|
|
|
325
329
|
---
|
|
326
330
|
|
|
@@ -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,29 +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.
|
|
430
|
+
|
|
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.
|
|
413
432
|
|
|
414
|
-
|
|
433
|
+
### Routing, validation, and docs
|
|
415
434
|
|
|
416
|
-
- Contract-first routing
|
|
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-wave9-audits.ts` static gates wired into CI as `pnpm verify:wave9-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-wave10-audits.ts` static gates wired into CI as `pnpm verify:wave10-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-wave11-audits.ts` wired into CI as `pnpm verify:wave11-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-wave12-audits.ts` wired into CI as `pnpm verify:wave12-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
|
-
- Production WebSocket routes under `secureDefaults` require a pre-upgrade `beforeUpgrade` decision hook or an explicit `acknowledgeUnauthenticated: true` marker, **AND** an Origin policy (`allowedOrigins: "same-origin"` / `string[]` / predicate) or `acknowledgeCrossOriginUpgrade: true` since `0.33.0` to close 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, because browsers always attach cookies on the upgrade. The Origin check runs **before** `beforeUpgrade` in both Node and Bun adapters; `httpError({ res })` also uses case-insensitive header merging and never forwards a custom response's `Content-Length` onto the rendered `problem+json` body.
|
|
422
|
-
- 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.
|
|
423
|
-
- 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).
|
|
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`.
|
|
424
440
|
- In-process test client (`app.request()`), contract-test runner, in-process typed client, and Hey API codegen via `pnpm gen`.
|
|
425
|
-
- One-command watch loop: `daloy dev` 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.
|
|
426
|
-
- AI-friendly route metadata via an optional `meta: { examples, extensions, summary, description, tags }` field on `route()`; examples are validated against your Standard Schemas at build time, surfaced into OpenAPI as `examples` + `x-daloy-*` extensions, and dumped as a sibling `routes.json` (or `routes.yaml` via `--yaml` / `--format yaml`, ~30% fewer LLM tokens) through `daloy inspect --ai` for LLM / Hey API / codegen consumption.
|
|
427
|
-
- Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno) — explicit `openapi.info` values always win.
|
|
428
|
-
- Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true`, with Scalar UI theming/custom CSS via `docs.scalar` — covers Swagger UI's `swagger.yaml` convention out of the box.
|
|
429
|
-
- `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.
|
|
430
|
-
- Plugin encapsulation, decorators, structured logging, request-id propagation, lifecycle events (`onPluginInstalled`, `onShutdown`, `onClose`), and graceful shutdown.
|
|
431
|
-
- Integration guides for transactional email providers — AWS SES, SendGrid, Resend, Postmark, Mailgun, and Mailtrap — with a common `EmailSender` plugin pattern and runtime-compatibility matrix.
|
|
432
|
-
- 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.
|
|
433
441
|
|
|
434
|
-
|
|
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).
|
|
435
554
|
|
|
436
555
|
## Contributing
|
|
437
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
package/dist/adapters/node.js
CHANGED
|
@@ -4,30 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createServer, } from "node:http";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
|
+
import { DALOY_RAW_BODY } from "../app.js";
|
|
7
8
|
import { FrameSink, encodeFrame, encodeClosePayload, encodeSendPayload, validateUpgrade, validateSelectedSubprotocol, checkWebSocketOrigin, WS_OPCODE, WS_CLOSE_CODE, WS_READY_STATE, WS_MAX_CONTROL_PAYLOAD, WebSocketProtocolError, WebSocketPayloadTooLargeError, } from "../websocket.js";
|
|
8
9
|
/** Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}. */
|
|
9
10
|
export function serve(app, opts = {}) {
|
|
10
11
|
const trustProxy = opts.trustProxy === true;
|
|
11
|
-
const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 },
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, (req, res) => {
|
|
13
|
+
// GET/HEAD: no body work, dispatch directly. Keep this first so the GET
|
|
14
|
+
// hot path doesn't pay for any of the buffering bookkeeping below.
|
|
15
|
+
const method = req.method;
|
|
16
|
+
if (method === "GET" || method === "HEAD" || method === undefined) {
|
|
17
|
+
dispatchToApp(app, req, res, trustProxy, undefined);
|
|
18
|
+
return;
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
res.destroy(e);
|
|
29
|
-
}
|
|
20
|
+
// POST/PUT/PATCH/DELETE with a small known content-length: pre-buffer
|
|
21
|
+
// bytes from the Node socket directly so the Request constructor gets a
|
|
22
|
+
// Uint8Array body instead of `Readable.toWeb(req)`. This skips the
|
|
23
|
+
// WHATWG-stream adapter that dominates POST throughput on Node.
|
|
24
|
+
const cl = req.headers["content-length"];
|
|
25
|
+
const n = cl ? Number(cl) : NaN;
|
|
26
|
+
if (Number.isFinite(n) && n >= 0 && n <= BUFFERED_BODY_MAX_BYTES) {
|
|
27
|
+
bufferRequestBody(req, n).then((bytes) => dispatchToApp(app, req, res, trustProxy, bytes), (e) => writeAdapterError(res, e));
|
|
28
|
+
return;
|
|
30
29
|
}
|
|
30
|
+
dispatchToApp(app, req, res, trustProxy, undefined);
|
|
31
31
|
});
|
|
32
32
|
server.requestTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
33
33
|
server.headersTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
@@ -69,33 +69,145 @@ export function serve(app, opts = {}) {
|
|
|
69
69
|
}
|
|
70
70
|
return { server, port, close };
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Maximum content-length (in bytes) that the Node adapter will pre-buffer
|
|
74
|
+
* before constructing the `Request`. Bodies above this fall back to the
|
|
75
|
+
* streaming `Readable.toWeb` path so unbounded uploads can't exhaust
|
|
76
|
+
* adapter memory. 1 MiB matches the default `App.bodyLimitBytes`.
|
|
77
|
+
*/
|
|
78
|
+
const BUFFERED_BODY_MAX_BYTES = 1024 * 1024;
|
|
79
|
+
function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
|
|
80
|
+
let request;
|
|
81
|
+
try {
|
|
82
|
+
request = toWebRequest(req, trustProxy, bufferedBody);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
writeAdapterError(res, e);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const responseOrPromise = app.fetch(request);
|
|
89
|
+
if (responseOrPromise instanceof Promise) {
|
|
90
|
+
responseOrPromise.then((response) => {
|
|
91
|
+
try {
|
|
92
|
+
const sent = sendWebResponse(response, res);
|
|
93
|
+
if (sent instanceof Promise)
|
|
94
|
+
sent.catch((e) => writeAdapterError(res, e));
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
writeAdapterError(res, e);
|
|
98
|
+
}
|
|
99
|
+
}, (e) => writeAdapterError(res, e));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
try {
|
|
103
|
+
const sent = sendWebResponse(responseOrPromise, res);
|
|
104
|
+
if (sent instanceof Promise)
|
|
105
|
+
sent.catch((e) => writeAdapterError(res, e));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
writeAdapterError(res, e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function bufferRequestBody(req, expected) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let received = 0;
|
|
116
|
+
let settled = false;
|
|
117
|
+
const onData = (chunk) => {
|
|
118
|
+
if (settled)
|
|
119
|
+
return;
|
|
120
|
+
received += chunk.length;
|
|
121
|
+
if (received > expected) {
|
|
122
|
+
settled = true;
|
|
123
|
+
cleanup();
|
|
124
|
+
req.destroy();
|
|
125
|
+
reject(new Error("Request body exceeded declared Content-Length"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
chunks.push(chunk);
|
|
129
|
+
};
|
|
130
|
+
const onEnd = () => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
if (received === 0) {
|
|
136
|
+
resolve(new Uint8Array(0));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const buf = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, received);
|
|
140
|
+
resolve(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
141
|
+
};
|
|
142
|
+
const onErr = (err) => {
|
|
143
|
+
if (settled)
|
|
144
|
+
return;
|
|
145
|
+
settled = true;
|
|
146
|
+
cleanup();
|
|
147
|
+
reject(err);
|
|
148
|
+
};
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
req.off("data", onData);
|
|
151
|
+
req.off("end", onEnd);
|
|
152
|
+
req.off("error", onErr);
|
|
153
|
+
req.off("aborted", onErr);
|
|
154
|
+
};
|
|
155
|
+
req.on("data", onData);
|
|
156
|
+
req.on("end", onEnd);
|
|
157
|
+
req.on("error", onErr);
|
|
158
|
+
req.on("aborted", onErr);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function writeAdapterError(res, e) {
|
|
162
|
+
if (!res.headersSent) {
|
|
163
|
+
res.statusCode = 500;
|
|
164
|
+
res.setHeader("content-type", "application/problem+json");
|
|
165
|
+
res.end(JSON.stringify({
|
|
166
|
+
type: "https://daloyjs.dev/errors/internal",
|
|
167
|
+
title: "Internal Server Error",
|
|
168
|
+
status: 500,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
res.destroy(e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function toWebRequest(req, trustProxy, bufferedBody) {
|
|
176
|
+
const reqHeaders = req.headers;
|
|
73
177
|
const forwardedHost = trustProxy
|
|
74
|
-
? firstHeader(
|
|
178
|
+
? firstHeader(reqHeaders["x-forwarded-host"])
|
|
75
179
|
: undefined;
|
|
76
|
-
const host = forwardedHost ??
|
|
180
|
+
const host = forwardedHost ?? reqHeaders.host ?? "localhost";
|
|
77
181
|
const forwardedProto = trustProxy
|
|
78
|
-
? firstHeader(
|
|
182
|
+
? firstHeader(reqHeaders["x-forwarded-proto"])
|
|
79
183
|
: undefined;
|
|
80
184
|
const proto = forwardedProto ??
|
|
81
185
|
(req.socket.encrypted ? "https" : "http");
|
|
82
186
|
const url = `${proto}://${host}${req.url ?? "/"}`;
|
|
83
187
|
const headers = new Headers();
|
|
84
|
-
for (const
|
|
188
|
+
for (const k in reqHeaders) {
|
|
189
|
+
const v = reqHeaders[k];
|
|
85
190
|
if (v === undefined)
|
|
86
191
|
continue;
|
|
87
|
-
|
|
88
|
-
headers.set(k, v.join(", "));
|
|
89
|
-
else
|
|
90
|
-
headers.set(k, String(v));
|
|
192
|
+
headers.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
91
193
|
}
|
|
92
194
|
const method = req.method ?? "GET";
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
init.body = Readable.toWeb(req);
|
|
96
|
-
init.duplex = "half";
|
|
195
|
+
if (method === "GET" || method === "HEAD") {
|
|
196
|
+
return new Request(url, { method, headers });
|
|
97
197
|
}
|
|
98
|
-
|
|
198
|
+
if (bufferedBody !== undefined) {
|
|
199
|
+
return new Request(url, {
|
|
200
|
+
method,
|
|
201
|
+
headers,
|
|
202
|
+
body: bufferedBody,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return new Request(url, {
|
|
206
|
+
method,
|
|
207
|
+
headers,
|
|
208
|
+
body: Readable.toWeb(req),
|
|
209
|
+
duplex: "half",
|
|
210
|
+
});
|
|
99
211
|
}
|
|
100
212
|
function firstHeader(v) {
|
|
101
213
|
if (v === undefined)
|
|
@@ -106,14 +218,43 @@ function firstHeader(v) {
|
|
|
106
218
|
const comma = raw.indexOf(",");
|
|
107
219
|
return (comma === -1 ? raw : raw.slice(0, comma)).trim() || undefined;
|
|
108
220
|
}
|
|
109
|
-
|
|
221
|
+
function sendWebResponse(res, out) {
|
|
110
222
|
out.statusCode = res.status;
|
|
111
223
|
res.headers.forEach((v, k) => out.setHeader(k, v));
|
|
224
|
+
// Fast-path: response was produced by serializeResult and carries the raw
|
|
225
|
+
// body bytes via the DALOY_RAW_BODY Symbol. Skip arrayBuffer() and the
|
|
226
|
+
// reader-loop microtask churn entirely for buffer-backed responses.
|
|
227
|
+
const raw = res[DALOY_RAW_BODY];
|
|
228
|
+
if (raw !== undefined) {
|
|
229
|
+
if (raw === null) {
|
|
230
|
+
out.end();
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
out.end(raw);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
112
237
|
if (!res.body) {
|
|
113
238
|
out.end();
|
|
114
239
|
return;
|
|
115
240
|
}
|
|
116
|
-
const
|
|
241
|
+
const ct = out.getHeader("content-type");
|
|
242
|
+
const isStream = ct && typeof ct === "string" && ct.startsWith("text/event-stream");
|
|
243
|
+
const rawLength = out.getHeader("content-length");
|
|
244
|
+
const contentLength = typeof rawLength === "number"
|
|
245
|
+
? rawLength
|
|
246
|
+
: typeof rawLength === "string"
|
|
247
|
+
? Number(rawLength)
|
|
248
|
+
: Number.NaN;
|
|
249
|
+
if (!isStream && Number.isFinite(contentLength) && contentLength <= 64 * 1024) {
|
|
250
|
+
return res.arrayBuffer().then((ab) => {
|
|
251
|
+
out.end(Buffer.from(ab));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return pumpBody(res.body, out);
|
|
255
|
+
}
|
|
256
|
+
async function pumpBody(body, out) {
|
|
257
|
+
const reader = body.getReader();
|
|
117
258
|
while (true) {
|
|
118
259
|
const { done, value } = await reader.read();
|
|
119
260
|
if (done)
|
|
@@ -390,4 +531,3 @@ class NodeWebSocketConnection {
|
|
|
390
531
|
}
|
|
391
532
|
}
|
|
392
533
|
}
|
|
393
|
-
//# sourceMappingURL=node.js.map
|