@daloyjs/core 1.0.0-rc.5 → 1.0.0-rc.6
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 +22 -12
- package/dist/adapters/bun.js +1 -2
- package/dist/adapters/node.js +16 -30
- package/dist/app.d.ts +5 -1
- package/dist/app.js +74 -6
- package/dist/auto-ban.js +1 -3
- package/dist/cli.js +9 -6
- package/dist/config.js +1 -3
- package/dist/errors.js +2 -5
- package/dist/etag.js +12 -2
- package/dist/geo-block.js +4 -9
- package/dist/hashing.js +1 -1
- package/dist/http-signatures.js +3 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ip-reputation.js +1 -1
- package/dist/ip-restriction.js +3 -12
- package/dist/jwt.js +12 -14
- package/dist/logger.js +1 -3
- package/dist/multipart.js +9 -12
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +2 -2
- package/dist/rate-limit-redis.d.ts +4 -4
- package/dist/response-cache.d.ts +179 -21
- package/dist/response-cache.js +338 -29
- package/dist/safe-redirect.js +3 -1
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security-schemes.js +1 -2
- package/dist/subdomains.js +1 -4
- package/dist/tenancy.d.ts +40 -0
- package/dist/tenancy.js +54 -3
- package/dist/waf.js +40 -8
- package/dist/webhook-delivery.js +19 -3
- package/dist/websocket.d.ts +8 -0
- package/dist/websocket.js +19 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,11 +28,21 @@ Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yello
|
|
|
28
28
|
|
|
29
29
|
DaloyJS is maintained in the GitHub organization at <https://github.com/daloyjs>; the canonical framework repository is <https://github.com/daloyjs/daloy>.
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Acknowledgements
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
We are grateful to the following companies for supporting DaloyJS with free access to their tools and services.
|
|
34
|
+
|
|
35
|
+
<div align="center" style="background-color: #f5f5f5; padding: 25px; border-radius: 10px; margin: 20px 0;">
|
|
36
|
+
|
|
37
|
+
<a href="https://snyk.io"><img src="https://github.com/user-attachments/assets/da58db43-67cc-45d4-ade5-bdaa7b041465" height="75" width="auto" alt="Snyk"></a>
|
|
38
|
+
|
|
39
|
+
<a href="https://socket.dev"><img src="https://github.com/user-attachments/assets/7d2dde1f-6b60-4f20-b05b-80ace5ae6862" height="75" width="auto" alt="Socket"></a>
|
|
40
|
+
|
|
41
|
+
<a href="https://www.aikido.dev"><img src="https://github.com/user-attachments/assets/67e62dd1-b907-4246-a0aa-95ef13fa491c" height="75" width="auto" alt="Aikido"></a>
|
|
42
|
+
|
|
43
|
+
<a href="https://www.coderabbit.ai"><img src="https://github.com/user-attachments/assets/0d9d8e68-eb21-41ec-978c-337b66ee34b6" height="75" width="auto" alt="CodeRabbit"></a>
|
|
44
|
+
|
|
45
|
+
</div>
|
|
36
46
|
|
|
37
47
|
---
|
|
38
48
|
|
|
@@ -66,7 +76,7 @@ DaloyJS exists to be the framework you'd build if you took the best ideas from e
|
|
|
66
76
|
| **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. |
|
|
67
77
|
|
|
68
78
|
```
|
|
69
|
-
framework test suite passing · ≥90% line + function coverage / ≥
|
|
79
|
+
framework test suite passing · ≥90% line + function coverage / ≥92% branch coverage · typechecks on TypeScript 7 with `strict: true`
|
|
70
80
|
runs on Node, Bun, Deno, Cloudflare, Vercel
|
|
71
81
|
~12.3M static-route ops/sec · ~1.5M dynamic-route ops/sec on M-class CPU
|
|
72
82
|
```
|
|
@@ -196,7 +206,7 @@ app.get(
|
|
|
196
206
|
async ({ params }) => ({
|
|
197
207
|
status: 200,
|
|
198
208
|
body: { id: params.id, title: `Book ${params.id}` },
|
|
199
|
-
})
|
|
209
|
+
})
|
|
200
210
|
);
|
|
201
211
|
|
|
202
212
|
serve(app, { port: 3000 });
|
|
@@ -529,7 +539,7 @@ const usersPlugin = {
|
|
|
529
539
|
operationId: "me",
|
|
530
540
|
responses: { 200: { description: "ok" } },
|
|
531
541
|
},
|
|
532
|
-
async () => ({ status: 200, body: { user: "alice" } })
|
|
542
|
+
async () => ({ status: 200, body: { user: "alice" } })
|
|
533
543
|
);
|
|
534
544
|
},
|
|
535
545
|
};
|
|
@@ -574,9 +584,9 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
|
|
|
574
584
|
|
|
575
585
|
## Status
|
|
576
586
|
|
|
577
|
-
DaloyJS is at **`1.0.0-rc.
|
|
587
|
+
DaloyJS is at **`1.0.0-rc.6`**, a security-hardening release candidate carrying the remediations from a live over-the-wire engagement against a realistic multi-tenant app — cross-principal response-cache disclosure (`SECURITY-AUDIT.md` F-4 … F-9), SQLi signature evasions in `waf()`, and an unbounded WebSocket frame buffer. Because the framework has no external users yet, this RC makes a few intentional changes (see the [CHANGELOG](CHANGELOG.md)) to get the secure-by-default posture right before the stable release rather than deferring them; the generated OpenAPI contract is unchanged. From `1.0.0` stable onward, the API follows SemVer with deprecations getting at least one minor cycle. The framework is already in use for production trials.
|
|
578
588
|
|
|
579
|
-
**Release quality bar.** Every release ships with **≥90% line + function coverage and ≥
|
|
589
|
+
**Release quality bar.** Every release ships with **≥90% line + function coverage** (`pnpm coverage`) **and ≥92% branch coverage** (`pnpm coverage:branches` on compiled JS), 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.
|
|
580
590
|
|
|
581
591
|
### Routing, validation, and docs
|
|
582
592
|
|
|
@@ -646,11 +656,11 @@ The framework refuses to start (or to construct) when configuration is unsafe:
|
|
|
646
656
|
- `requireScopes()` with RFC-6750 `WWW-Authenticate: Bearer` challenge and per-request scope aggregation.
|
|
647
657
|
- `session()` with signed cookies and pluggable stores.
|
|
648
658
|
- `idempotency()` with `Idempotency-Key` fingerprinting + byte-for-byte response replay, in-flight `409`, `422` on key reuse with a different payload, and a pluggable `IdempotencyStore` (in-memory default) at `@daloyjs/core/idempotency`.
|
|
649
|
-
- `responseCache()` server-side body cache (cache-key + TTL with `s-maxage`/`max-age` orchestration, request `no-store`/`no-cache` directives, recursion-safe stale-while-revalidate, `
|
|
659
|
+
- `responseCache()` server-side body cache (cache-key + TTL with `s-maxage`/`max-age` orchestration, request `no-store`/`no-cache` directives, recursion-safe stale-while-revalidate, proactive `varyHeaders` keying, `X-Cache` HIT/MISS/STALE marker, pluggable `ResponseCacheStore` whose in-memory default is bounded on both entry count and retained bytes) at `@daloyjs/core/response-cache`. Never caches `Set-Cookie`, `private`/`no-store`/`no-cache`, or `Vary: *` responses, and strips `Age`/hop-by-hop/`X-Request-Id` from stored entries so a hit never replays another request's correlation id. **Fail-closed on every principal dimension (CWE-524):** the key is the full _effective request URI_ including the authority (RFC 9111 §4), so hostnames never share entries; requests carrying `Authorization` **or** `Cookie` bypass the shared cache unless a `principal` names the caller (then each gets its own entry) or the header is explicitly declared shareable; a tenant resolved by `tenancy()` is folded into the key automatically — with a boot guard that refuses to start if the cache is mounted ahead of `tenancy()`; and the response's **own `Vary` header** is honoured as a secondary key (RFC 9111 §4.1), so the `Vary: Origin` written by `cors()` and the `Vary: Accept-Encoding` written by `compression()` keep one caller's allowed origin — or their gzipped bytes — from being served to the next, with each variant stored separately so they all stay warm. Complements `etag()`/`compression()`, which do not cache bodies.
|
|
650
660
|
- `paginationQuery()` / `encodeCursor()` / `decodeCursor()` / `buildPageLinks()` / `buildLinkHeader()` cursor-pagination helpers at `@daloyjs/core/pagination`: opaque base64url cursors (length-capped, prototype-pollution-safe decode → `400` on tamper), RFC 8288 `Link` header emission with CRLF / header-injection guards, and a Standard Schema that validates `cursor`/`limit` and auto-wires both into the OpenAPI spec + typed client via `toJSONSchema()`.
|
|
651
661
|
- `app.metrics()` + `MetricsRegistry` / `httpMetrics()` Prometheus / OpenMetrics exposition at `@daloyjs/core/metrics`: dependency-free counters / gauges / histograms, RED instrumentation (`http_requests_total`, `http_request_duration_seconds`, `http_requests_in_flight`) plus process gauges, exposition-injection-safe name/label validation, a per-metric cardinality cap, and an opt-in `/metrics` route with the same hardened posture as `app.healthcheck()` (bearer token + `timingSafeEqual`, per-IP rate limit, refuse-to-boot unauthenticated in production). The repo ships an `examples/observability/` Docker Compose stack that starts a pre-configured Prometheus + Grafana pair (with an auto-provisioned RED + heatmap dashboard) against any local app via `docker compose -f examples/observability/docker-compose.yml up`.
|
|
652
662
|
- `otelTracing()` OpenTelemetry-compatible distributed tracing at `@daloyjs/core/tracing`: a dependency-free `Hooks` bundle that opens one `SERVER` span per request, attaches HTTP semantic-convention attributes (`http.request.method`, `url.path`, `server.address` / `server.port`, `http.response.status_code`, …), records exceptions + escalates `5xx` to `ERROR`, guarantees a single `span.end()`, and exposes the live span on `ctx.state.otelSpan`. Bring any tracer matching the small `TracingTracer` interface (the real `@opentelemetry/api` SDK on Node, or a custom exporter on Workers/Deno) plus your own propagator via `contextFromRequest` for `traceparent` continuation — no OTel SDK is forced into your install. The `examples/observability/` stack also runs **Jaeger**, and `examples/otel-tracing-demo.ts` ships a ~120-line dependency-free OTLP/HTTP exporter that streams spans straight to it.
|
|
653
|
-
- `tenancy()` secure-by-default multitenancy at `@daloyjs/core/tenancy`: a dependency-free `Hooks` bundle that resolves the calling tenant once per request and exposes it on `ctx.state.tenant`. Pluggable resolution (`tenantFromSubdomain` PSL-aware, `tenantFromHeader`, `tenantFromPathPrefix`, `tenantFromClaim`, or a custom `(ctx) => string`, tried in array order). **Refuse-unresolved by default** (no ambient "default" tenant leak), **format-validated ids** (rejects key/log-injection + cache-poisoning payloads before they reach a key), **no-enumeration `404`** for unknown tenants, and **host-spoof-safe** subdomain resolution. A `tenantScope()` key helper drops straight into `rateLimit` `keyGenerator` and `concurrencyLimit` / `idempotency`
|
|
663
|
+
- `tenancy()` secure-by-default multitenancy at `@daloyjs/core/tenancy`: a dependency-free `Hooks` bundle that resolves the calling tenant once per request and exposes it on `ctx.state.tenant`. Pluggable resolution (`tenantFromSubdomain` PSL-aware, `tenantFromHeader`, `tenantFromPathPrefix`, `tenantFromClaim`, or a custom `(ctx) => string`, tried in array order). **Refuse-unresolved by default** (no ambient "default" tenant leak), **format-validated ids** (rejects key/log-injection + cache-poisoning payloads before they reach a key), **no-enumeration `404`** for unknown tenants, and **host-spoof-safe** subdomain resolution. A `tenantScope()` key helper drops straight into `rateLimit` `keyGenerator` and `concurrencyLimit` / `idempotency` `scope` to partition each per tenant (CWE-524 cross-tenant cache defense); `responseCache()` needs no wiring at all — it reads the resolved tenant itself and refuses to boot if mounted ahead of `tenancy()`. Runnable `examples/multitenancy-demo.ts`.
|
|
654
664
|
- `resilientFetch()` + `CircuitBreaker` outbound resilience at `@daloyjs/core/fetch-resilience`: a dependency-free circuit breaker (`closed → open → half-open`), retry-with-backoff (exponential + full jitter, idempotent-method/transient-status scoped, honours `Retry-After`), and a per-call timeout (`AbortController` → `FetchTimeoutError`) designed to layer **on top of** `fetchGuard()` — an `SsrfBlockedError` is a terminal refusal that is never retried and never trips the breaker, so SSRF protection stays intact under the resilience layer.
|
|
655
665
|
- `createWebhookSender()` + `MemoryWebhookDeadLetterSink` outbound webhook delivery at `@daloyjs/core/webhook-delivery`: the outbound counterpart to `verifyWebhookSignature()` — timestamped HMAC-signed `POST`s (`webhook-id` / `webhook-timestamp` / `webhook-signature`, computed over `"<timestamp>.<body>"` and reused across retries for safe deduping), bounded retry-with-backoff (transient-status + network scoped, honours `Retry-After`), per-attempt timeout, and dead-letter semantics. Transport defaults to `fetchGuard()`, so a subscriber URL pointing at cloud metadata or a private range is refused with a terminal `SsrfBlockedError` (never retried, dead-lettered once). Zero runtime dependencies.
|
|
656
666
|
- `app.cron()` + standalone `Scheduler` in-process scheduled tasks at `@daloyjs/core/scheduler`: a queue-agnostic schedule primitive for periodic housekeeping (cache sweeps, token refresh, reconciliation). Fixed intervals or 5-field cron expressions (lists / ranges / steps / named months & days / `@hourly`–`@yearly` aliases / optional IANA `timeZone`), arithmetic cron parsing (no backtracking regex), fixed-rate **single-flight** (overlapping ticks are skipped, never run concurrently), per-run `timeoutMs` with `AbortSignal`, and graceful-shutdown integration (stop arming → await in-flight → abort after grace). Timers are `unref`'d. `parseCron()` / `nextCronRun()` exported standalone. Zero runtime dependencies.
|
|
@@ -697,7 +707,7 @@ The framework refuses to start (or to construct) when configuration is unsafe:
|
|
|
697
707
|
- Adapter-independent `ConnInfo` abstraction: `getConnInfo()`, lazy `ctx.remoteAddress`, `ctx.remotePort` — populated by the Node, Bun, Deno, and Lambda adapters from the real peer socket / event source, never from spoofable headers.
|
|
698
708
|
- `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).
|
|
699
709
|
- PSL-aware `subdomains()` helper with a `≤ 90 days` snapshot guard.
|
|
700
|
-
- Secure-by-default multitenancy via `tenancy()` + `tenantScope()`: pluggable tenant resolution (subdomain / header / path / JWT claim / custom), refuse-unresolved + format-validated ids + no-enumeration `404` by default,
|
|
710
|
+
- Secure-by-default multitenancy via `tenancy()` + `tenantScope()`: pluggable tenant resolution (subdomain / header / path / JWT claim / custom), refuse-unresolved + format-validated ids + no-enumeration `404` by default, a key helper that partitions `rateLimit` / `concurrencyLimit` / `idempotency` per tenant, and automatic per-tenant `responseCache` partitioning backed by a boot guard.
|
|
701
711
|
- `defineDependency()` typed-DI helper with per-request deduplication.
|
|
702
712
|
- Scheme-aware `ctx.state.auth` typed contract; named, optionally seeded stateful plugins.
|
|
703
713
|
|
package/dist/adapters/bun.js
CHANGED
|
@@ -273,8 +273,7 @@ function reportBunHandlerFailure(app, data, label, err, notifyError) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
function validateControlPayload(data) {
|
|
276
|
-
if (data !== undefined &&
|
|
277
|
-
encodeSendPayload(data).payload.length > WS_MAX_CONTROL_PAYLOAD) {
|
|
276
|
+
if (data !== undefined && encodeSendPayload(data).payload.length > WS_MAX_CONTROL_PAYLOAD) {
|
|
278
277
|
throw new WebSocketProtocolError("Control frame payload exceeds 125 bytes");
|
|
279
278
|
}
|
|
280
279
|
}
|
package/dist/adapters/node.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Node adapter: translates IncomingMessage/ServerResponse to web-standard
|
|
3
3
|
* Request/Response. Includes graceful shutdown wired to SIGTERM/SIGINT.
|
|
4
4
|
*/
|
|
5
|
-
import { createServer
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY, DALOY_LIGHT_RESPONSE_OK, DALOY_REQUEST_ABORT, } from "../app.js";
|
|
8
8
|
import { BadRequestError } from "../errors.js";
|
|
@@ -74,9 +74,7 @@ export function serve(app, opts = {}) {
|
|
|
74
74
|
// `0` opts out and restores Node's unbounded-ish default (2000).
|
|
75
75
|
const maxHeaderCount = opts.maxHeaderCount;
|
|
76
76
|
server.maxHeadersCount =
|
|
77
|
-
typeof maxHeaderCount === "number" && maxHeaderCount >= 0
|
|
78
|
-
? maxHeaderCount
|
|
79
|
-
: 100;
|
|
77
|
+
typeof maxHeaderCount === "number" && maxHeaderCount >= 0 ? maxHeaderCount : 100;
|
|
80
78
|
// Connection-layer admission control. Reject overflow sockets at accept time
|
|
81
79
|
// rather than queuing them into the event loop under overload.
|
|
82
80
|
if (typeof opts.maxConnections === "number" && opts.maxConnections > 0) {
|
|
@@ -125,7 +123,10 @@ export function serve(app, opts = {}) {
|
|
|
125
123
|
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
126
124
|
};
|
|
127
125
|
if (opts.handleSignals !== false) {
|
|
128
|
-
const onSignal = (sig) => {
|
|
126
|
+
const onSignal = (sig) => {
|
|
127
|
+
app.log.info({ sig }, "DaloyJS received signal, shutting down");
|
|
128
|
+
void close().then(() => process.exit(0));
|
|
129
|
+
};
|
|
129
130
|
process.once("SIGTERM", () => onSignal("SIGTERM"));
|
|
130
131
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
131
132
|
}
|
|
@@ -133,9 +134,7 @@ export function serve(app, opts = {}) {
|
|
|
133
134
|
server,
|
|
134
135
|
get port() {
|
|
135
136
|
const address = server.address();
|
|
136
|
-
return address !== null && typeof address === "object"
|
|
137
|
-
? address.port
|
|
138
|
-
: requestedPort;
|
|
137
|
+
return address !== null && typeof address === "object" ? address.port : requestedPort;
|
|
139
138
|
},
|
|
140
139
|
close,
|
|
141
140
|
};
|
|
@@ -280,11 +279,7 @@ function attachClientCertificate(req, request) {
|
|
|
280
279
|
* Node's `connect` event rather than the request listener; it is included here
|
|
281
280
|
* defensively for runtimes/proxies that surface it as a normal request.)
|
|
282
281
|
*/
|
|
283
|
-
const FETCH_FORBIDDEN_METHODS = new Set([
|
|
284
|
-
"CONNECT",
|
|
285
|
-
"TRACE",
|
|
286
|
-
"TRACK",
|
|
287
|
-
]);
|
|
282
|
+
const FETCH_FORBIDDEN_METHODS = new Set(["CONNECT", "TRACE", "TRACK"]);
|
|
288
283
|
/**
|
|
289
284
|
* Refuse a Fetch-forbidden HTTP method with a spec-correct `501 Not
|
|
290
285
|
* Implemented`. `501` is more accurate than `405` here because the method is
|
|
@@ -544,15 +539,10 @@ Object.setPrototypeOf(LightRequest.prototype, Request.prototype);
|
|
|
544
539
|
LightRequest.prototype[DALOY_LIGHT_RESPONSE_OK] = true;
|
|
545
540
|
function toWebRequest(req, trustProxy, bufferedBody) {
|
|
546
541
|
const reqHeaders = req.headers;
|
|
547
|
-
const forwardedHost = trustProxy
|
|
548
|
-
? firstHeader(reqHeaders["x-forwarded-host"])
|
|
549
|
-
: undefined;
|
|
542
|
+
const forwardedHost = trustProxy ? firstHeader(reqHeaders["x-forwarded-host"]) : undefined;
|
|
550
543
|
const host = forwardedHost ?? reqHeaders.host ?? "localhost";
|
|
551
|
-
const forwardedProto = trustProxy
|
|
552
|
-
|
|
553
|
-
: undefined;
|
|
554
|
-
const proto = forwardedProto ??
|
|
555
|
-
(req.socket.encrypted ? "https" : "http");
|
|
544
|
+
const forwardedProto = trustProxy ? firstHeader(reqHeaders["x-forwarded-proto"]) : undefined;
|
|
545
|
+
const proto = forwardedProto ?? (req.socket.encrypted ? "https" : "http");
|
|
556
546
|
const url = `${proto}://${host}${normalizeRequestTarget(req.url)}`;
|
|
557
547
|
// Reject malformed Host / request-target combinations at the adapter
|
|
558
548
|
// boundary instead of letting the invalid URL propagate as a 500 later.
|
|
@@ -667,7 +657,8 @@ function sendWebResponse(res, out) {
|
|
|
667
657
|
// which we must strip so Node falls back to chunked transfer-encoding.
|
|
668
658
|
const rawStream = res[DALOY_RAW_STREAM];
|
|
669
659
|
if (rawStream !== undefined) {
|
|
670
|
-
if (typeof rawStream.pipe === "function" &&
|
|
660
|
+
if (typeof rawStream.pipe === "function" &&
|
|
661
|
+
!(rawStream instanceof ReadableStream)) {
|
|
671
662
|
// Node `Readable` from the handler: skip the Web-stream bridge entirely
|
|
672
663
|
// and `.pipe(out)` like Fastify/Koa/Express do.
|
|
673
664
|
out.removeHeader("content-length");
|
|
@@ -721,15 +712,10 @@ function pumpBody(body, out) {
|
|
|
721
712
|
}
|
|
722
713
|
// ---------- WebSocket upgrade ----------
|
|
723
714
|
async function handleUpgrade(app, req, socket, head, trustProxy) {
|
|
724
|
-
const forwardedHost = trustProxy
|
|
725
|
-
? firstHeader(req.headers["x-forwarded-host"])
|
|
726
|
-
: undefined;
|
|
715
|
+
const forwardedHost = trustProxy ? firstHeader(req.headers["x-forwarded-host"]) : undefined;
|
|
727
716
|
const host = forwardedHost ?? req.headers.host ?? "localhost";
|
|
728
|
-
const forwardedProto = trustProxy
|
|
729
|
-
|
|
730
|
-
: undefined;
|
|
731
|
-
const proto = forwardedProto ??
|
|
732
|
-
(req.socket.encrypted ? "https" : "http");
|
|
717
|
+
const forwardedProto = trustProxy ? firstHeader(req.headers["x-forwarded-proto"]) : undefined;
|
|
718
|
+
const proto = forwardedProto ?? (req.socket.encrypted ? "https" : "http");
|
|
733
719
|
// A malformed `Host` header (e.g. containing a space) reaches this point:
|
|
734
720
|
// Node's HTTP parser accepts it and fires `upgrade`, but WHATWG URL
|
|
735
721
|
// parsing throws. Reject it as the client error it is instead of letting
|
package/dist/app.d.ts
CHANGED
|
@@ -1176,7 +1176,11 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
|
|
|
1176
1176
|
* auth hook unless it opted out with `mcpRoutes(path, handler, { public: true })`.
|
|
1177
1177
|
* MCP tools are model-controlled and side-effecting, so a public one is a
|
|
1178
1178
|
* high-impact default.
|
|
1179
|
-
* 3. **
|
|
1179
|
+
* 3. **Cache ahead of tenancy** — a `responseCache()` that runs before
|
|
1180
|
+
* `tenancy()` builds its key before the tenant exists in `ctx.state`, so
|
|
1181
|
+
* every tenant collides on one entry and one tenant's response is served to
|
|
1182
|
+
* the next caller (CWE-524).
|
|
1183
|
+
* 4. **Missing CSRF** — when `session()` is installed and any route accepts a
|
|
1180
1184
|
* state-changing method (`POST`/`PUT`/`PATCH`/`DELETE`), a `csrf()` hook
|
|
1181
1185
|
* (or third-party equivalent stamped with {@link CSRF_HOOK_MARKER}) must
|
|
1182
1186
|
* also be present. Skipped when `app({ csrf: "off" })`.
|
package/dist/app.js
CHANGED
|
@@ -127,6 +127,15 @@ const CANONICAL_HTTP_METHODS = new Set([
|
|
|
127
127
|
* `App` bundle. Must match the string used in `mcpRoutes`.
|
|
128
128
|
*/
|
|
129
129
|
const MCP_ROUTE_MARKER = Symbol.for("daloyjs.mcp.route");
|
|
130
|
+
/**
|
|
131
|
+
* Global-registry symbols stamped by `responseCache()` and `tenancy()` on the
|
|
132
|
+
* `Hooks` bundles they return. Read here — rather than imported from
|
|
133
|
+
* `response-cache.ts` / `tenancy.ts` — so neither module is pulled into the core
|
|
134
|
+
* `App` bundle (which would cost every serverless cold start). Must match the
|
|
135
|
+
* strings used in those modules.
|
|
136
|
+
*/
|
|
137
|
+
const RESPONSE_CACHE_HOOK_MARKER = Symbol.for("daloyjs.response-cache.hook");
|
|
138
|
+
const TENANCY_HOOK_MARKER = Symbol.for("daloyjs.tenancy.hook");
|
|
130
139
|
/**
|
|
131
140
|
* Apply a topology-aware security preset on top of caller-supplied
|
|
132
141
|
* options. Returns a new options object where preset defaults fill in
|
|
@@ -949,7 +958,11 @@ export class App {
|
|
|
949
958
|
* auth hook unless it opted out with `mcpRoutes(path, handler, { public: true })`.
|
|
950
959
|
* MCP tools are model-controlled and side-effecting, so a public one is a
|
|
951
960
|
* high-impact default.
|
|
952
|
-
* 3. **
|
|
961
|
+
* 3. **Cache ahead of tenancy** — a `responseCache()` that runs before
|
|
962
|
+
* `tenancy()` builds its key before the tenant exists in `ctx.state`, so
|
|
963
|
+
* every tenant collides on one entry and one tenant's response is served to
|
|
964
|
+
* the next caller (CWE-524).
|
|
965
|
+
* 4. **Missing CSRF** — when `session()` is installed and any route accepts a
|
|
953
966
|
* state-changing method (`POST`/`PUT`/`PATCH`/`DELETE`), a `csrf()` hook
|
|
954
967
|
* (or third-party equivalent stamped with {@link CSRF_HOOK_MARKER}) must
|
|
955
968
|
* also be present. Skipped when `app({ csrf: "off" })`.
|
|
@@ -998,7 +1011,24 @@ export class App {
|
|
|
998
1011
|
this.bootGuard.error = err;
|
|
999
1012
|
throw err;
|
|
1000
1013
|
}
|
|
1001
|
-
// Guard 3:
|
|
1014
|
+
// Guard 3: responseCache() mounted ahead of tenancy(). The cache partitions
|
|
1015
|
+
// on the tenant automatically, but only if the tenant is already in
|
|
1016
|
+
// ctx.state when the key is built. Mounted first, it would key every
|
|
1017
|
+
// tenant's response identically and serve one tenant's private body to the
|
|
1018
|
+
// next caller (CWE-524) — silently, with a normal-looking cache HIT.
|
|
1019
|
+
const cacheBeforeTenancy = this.routeSecurityMarkers.find((r) => r.cacheBeforeTenancy);
|
|
1020
|
+
if (cacheBeforeTenancy) {
|
|
1021
|
+
const err = new Error(`Route ${cacheBeforeTenancy.method} ${cacheBeforeTenancy.path} runs responseCache() ` +
|
|
1022
|
+
`before tenancy() in its effective hook chain. The cache key is built before the tenant ` +
|
|
1023
|
+
`is resolved, so every tenant would share one cache entry and one tenant's response ` +
|
|
1024
|
+
`would be served to the next caller (CWE-524 cross-tenant cached-response disclosure). ` +
|
|
1025
|
+
`Register tenancy() first — as a global hook (new App({ hooks: tenancy(...) })) or an ` +
|
|
1026
|
+
`earlier app.use(...) — so the tenant is in ctx.state before the cache reads it. ` +
|
|
1027
|
+
`See https://daloyjs.dev/docs/security/boot-guards.`);
|
|
1028
|
+
this.bootGuard.error = err;
|
|
1029
|
+
throw err;
|
|
1030
|
+
}
|
|
1031
|
+
// Guard 4: session() + state-changing route without csrf().
|
|
1002
1032
|
if (this.options.csrf === "off")
|
|
1003
1033
|
return;
|
|
1004
1034
|
const stateChanging = this.routeSecurityMarkers.find((r) => isStateChangingMethod(r.method) && r.hasSession && !r.hasCsrf);
|
|
@@ -1068,13 +1098,21 @@ export class App {
|
|
|
1068
1098
|
this.trustProxyWarned = true;
|
|
1069
1099
|
this.log.warn({ event: "trust-proxy.unconfigured", header: found }, `Request carried ${found} but app({ trustProxy }) is unset; refusing to honour spoofable proxy headers.`);
|
|
1070
1100
|
}
|
|
1071
|
-
|
|
1101
|
+
const refusal = new InternalError(`Refusing to dispatch request: ${found} header is present but app({ trustProxy }) is unconfigured. ` +
|
|
1072
1102
|
`Honouring a spoofable forwarded header would let a client forge its source IP for the rate ` +
|
|
1073
1103
|
`limiter, audit log, and request-id propagation. ` +
|
|
1074
1104
|
`Pass app({ trustProxy: true }) when running behind a trusted reverse proxy, ` +
|
|
1075
1105
|
`or app({ trustProxy: false }) to ignore forwarded headers, ` +
|
|
1076
1106
|
`or app({ secureDefaults: false }) to disable this guard. ` +
|
|
1077
1107
|
`See https://daloyjs.dev/docs/security/boot-guards.`);
|
|
1108
|
+
// Every refused request throws from this one line, so the stack is
|
|
1109
|
+
// identical each time and names framework internals rather than anything
|
|
1110
|
+
// an operator can act on. Keep the 500 and keep a line per request — the
|
|
1111
|
+
// refusal must stay visible — but drop the stack, so a client cannot
|
|
1112
|
+
// multiply the bytes it pushes into the error tier by replaying the header.
|
|
1113
|
+
// The actionable message is logged once per process by the warn above.
|
|
1114
|
+
refusal[OMIT_STACK_IN_LOG] = true;
|
|
1115
|
+
throw refusal;
|
|
1078
1116
|
}
|
|
1079
1117
|
/**
|
|
1080
1118
|
* Resolve the {@link AppOptions.docs} option and, when enabled, register
|
|
@@ -3143,16 +3181,29 @@ function securityMarkersFromHooks(layers) {
|
|
|
3143
3181
|
let hasSession = false;
|
|
3144
3182
|
let hasCsrf = false;
|
|
3145
3183
|
let hasAuth = false;
|
|
3146
|
-
|
|
3147
|
-
|
|
3184
|
+
// `layers` is in execution order, so the first index of each marker is enough
|
|
3185
|
+
// to tell whether the cache reads state before tenancy has written it.
|
|
3186
|
+
let cacheIndex = -1;
|
|
3187
|
+
let tenancyIndex = -1;
|
|
3188
|
+
for (let i = 0; i < layers.length; i++) {
|
|
3189
|
+
const record = layers[i];
|
|
3148
3190
|
if (record[SESSION_HOOK_MARKER] === true)
|
|
3149
3191
|
hasSession = true;
|
|
3150
3192
|
if (record[CSRF_HOOK_MARKER] === true)
|
|
3151
3193
|
hasCsrf = true;
|
|
3152
3194
|
if (record[AUTH_HOOK_MARKER] === true)
|
|
3153
3195
|
hasAuth = true;
|
|
3196
|
+
if (cacheIndex === -1 && record[RESPONSE_CACHE_HOOK_MARKER] === true)
|
|
3197
|
+
cacheIndex = i;
|
|
3198
|
+
if (tenancyIndex === -1 && record[TENANCY_HOOK_MARKER] === true)
|
|
3199
|
+
tenancyIndex = i;
|
|
3154
3200
|
}
|
|
3155
|
-
return {
|
|
3201
|
+
return {
|
|
3202
|
+
hasSession,
|
|
3203
|
+
hasCsrf,
|
|
3204
|
+
hasAuth,
|
|
3205
|
+
cacheBeforeTenancy: cacheIndex !== -1 && tenancyIndex !== -1 && cacheIndex < tenancyIndex,
|
|
3206
|
+
};
|
|
3156
3207
|
}
|
|
3157
3208
|
function isStateChangingMethod(method) {
|
|
3158
3209
|
return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
|
|
@@ -4079,8 +4130,25 @@ function withTimeout(p, ms, request) {
|
|
|
4079
4130
|
});
|
|
4080
4131
|
});
|
|
4081
4132
|
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Marker set on an error whose stack carries no incident information because
|
|
4135
|
+
* the same framework line throws it for every offending request — a rejected
|
|
4136
|
+
* *configuration* or *request shape*, not a fault in the app's code.
|
|
4137
|
+
*
|
|
4138
|
+
* `serializeErr` omits the stack for these. The stack is byte-identical on every
|
|
4139
|
+
* occurrence and points at framework internals, so logging it per request just
|
|
4140
|
+
* multiplies the volume an unauthenticated client can push into the error tier
|
|
4141
|
+
* (the expensive, alerting one) without telling an operator anything the
|
|
4142
|
+
* message does not already say.
|
|
4143
|
+
*
|
|
4144
|
+
* @internal
|
|
4145
|
+
*/
|
|
4146
|
+
const OMIT_STACK_IN_LOG = Symbol.for("daloyjs.error.omitStackInLog");
|
|
4082
4147
|
function serializeErr(err) {
|
|
4083
4148
|
if (err instanceof Error) {
|
|
4149
|
+
if (err[OMIT_STACK_IN_LOG] === true) {
|
|
4150
|
+
return { name: err.name, message: err.message };
|
|
4151
|
+
}
|
|
4084
4152
|
return { name: err.name, message: err.message, stack: err.stack };
|
|
4085
4153
|
}
|
|
4086
4154
|
return { value: String(err) };
|
package/dist/auto-ban.js
CHANGED
|
@@ -207,9 +207,7 @@ export function autoBan(opts = {}) {
|
|
|
207
207
|
opts.onStrike?.({ key, strikes, status: res.status });
|
|
208
208
|
if (strikes >= maxStrikes) {
|
|
209
209
|
banCount += 1;
|
|
210
|
-
const duration = escalate
|
|
211
|
-
? Math.min(maxBanMs, banMs * 2 ** (banCount - 1))
|
|
212
|
-
: banMs;
|
|
210
|
+
const duration = escalate ? Math.min(maxBanMs, banMs * 2 ** (banCount - 1)) : banMs;
|
|
213
211
|
bannedUntilMs = now + duration;
|
|
214
212
|
strikes = 0;
|
|
215
213
|
opts.onBan?.({ key, banCount, banDurationMs: duration, bannedUntilMs });
|
package/dist/cli.js
CHANGED
|
@@ -274,7 +274,11 @@ export function parseArgs(argv) {
|
|
|
274
274
|
};
|
|
275
275
|
let command = "inspect";
|
|
276
276
|
let i = 0;
|
|
277
|
-
if (argv[0] === "inspect" ||
|
|
277
|
+
if (argv[0] === "inspect" ||
|
|
278
|
+
argv[0] === "dev" ||
|
|
279
|
+
argv[0] === "help" ||
|
|
280
|
+
argv[0] === "doctor" ||
|
|
281
|
+
argv[0] === "diff") {
|
|
278
282
|
command = argv[0];
|
|
279
283
|
i = 1;
|
|
280
284
|
}
|
|
@@ -593,7 +597,8 @@ async function runDoctor(opts, io) {
|
|
|
593
597
|
const o = app.options;
|
|
594
598
|
const isProd = o.env === "production" ||
|
|
595
599
|
o.production === true ||
|
|
596
|
-
globalThis.process?.env?.NODE_ENV ===
|
|
600
|
+
globalThis.process?.env?.NODE_ENV ===
|
|
601
|
+
"production";
|
|
597
602
|
if (opts.noAuditDefaults !== true) {
|
|
598
603
|
if (isProd && o.trustProxy === undefined && o.behindProxy === undefined) {
|
|
599
604
|
findings.push({
|
|
@@ -907,10 +912,8 @@ export function buildAiDump(app, opts) {
|
|
|
907
912
|
method: def.method,
|
|
908
913
|
path: def.path,
|
|
909
914
|
...(def.operationId ? { operationId: def.operationId } : {}),
|
|
910
|
-
...(def.summary ?? meta?.summary
|
|
911
|
-
|
|
912
|
-
: {}),
|
|
913
|
-
...(def.description ?? meta?.description
|
|
915
|
+
...((def.summary ?? meta?.summary) ? { summary: def.summary ?? meta?.summary } : {}),
|
|
916
|
+
...((def.description ?? meta?.description)
|
|
914
917
|
? { description: def.description ?? meta?.description }
|
|
915
918
|
: {}),
|
|
916
919
|
tags: dedupeTags(def.tags, meta?.tags),
|
package/dist/config.js
CHANGED
|
@@ -24,9 +24,7 @@ export class ConfigValidationError extends Error {
|
|
|
24
24
|
/** Every validation issue, as `{ key, message }` pairs (`key` is the dotted path, `"<root>"`/`"<source>"` for top-level failures). */
|
|
25
25
|
issues;
|
|
26
26
|
constructor(issues) {
|
|
27
|
-
const summary = issues
|
|
28
|
-
.map((i) => ` - ${i.key || "<root>"}: ${i.message}`)
|
|
29
|
-
.join("\n");
|
|
27
|
+
const summary = issues.map((i) => ` - ${i.key || "<root>"}: ${i.message}`).join("\n");
|
|
30
28
|
super(`defineConfig(): configuration is invalid (${issues.length} issue${issues.length === 1 ? "" : "s"})\n${summary}`);
|
|
31
29
|
this.name = "ConfigValidationError";
|
|
32
30
|
this.issues = issues;
|
package/dist/errors.js
CHANGED
|
@@ -40,9 +40,7 @@ export class MessageLeakError extends Error {
|
|
|
40
40
|
/** The refused headers, each with its name and the reason it was disallowed. */
|
|
41
41
|
offendingHeaders;
|
|
42
42
|
constructor(offendingHeaders) {
|
|
43
|
-
const summary = offendingHeaders
|
|
44
|
-
.map((h) => `${h.name} (${h.reason})`)
|
|
45
|
-
.join(", ");
|
|
43
|
+
const summary = offendingHeaders.map((h) => `${h.name} (${h.reason})`).join(", ");
|
|
46
44
|
super(`httpError({ res }): custom error response carries disallowed header(s): ${summary}. ` +
|
|
47
45
|
"Only WWW-Authenticate, Proxy-Authenticate, Retry-After, Content-Type, " +
|
|
48
46
|
"Content-Language, Content-Length, and Cache-Control (no-store|no-cache) " +
|
|
@@ -212,8 +210,7 @@ export class HttpError extends Error {
|
|
|
212
210
|
* @returns A `Response` with `Content-Type: application/problem+json`.
|
|
213
211
|
*/
|
|
214
212
|
toResponse(opts = {}) {
|
|
215
|
-
const isProd = opts.production ??
|
|
216
|
-
(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
|
|
213
|
+
const isProd = opts.production ?? (typeof process !== "undefined" && process.env?.NODE_ENV === "production");
|
|
217
214
|
const out = { ...this.problem };
|
|
218
215
|
if (isProd && this.status >= 500) {
|
|
219
216
|
delete out.detail; // do not leak internals
|
package/dist/etag.js
CHANGED
|
@@ -38,7 +38,10 @@ async function sha1Hex(bytes) {
|
|
|
38
38
|
}
|
|
39
39
|
function inmMatches(headerValue, candidate) {
|
|
40
40
|
// RFC 7232 §3.2: comma-separated list of entity tags or `*`.
|
|
41
|
-
const list = headerValue
|
|
41
|
+
const list = headerValue
|
|
42
|
+
.split(",")
|
|
43
|
+
.map((s) => s.trim())
|
|
44
|
+
.filter((s) => s.length > 0);
|
|
42
45
|
if (list.length === 0)
|
|
43
46
|
return false;
|
|
44
47
|
for (const tag of list) {
|
|
@@ -97,7 +100,14 @@ export function etag(opts = {}) {
|
|
|
97
100
|
const inm = ctx?.request?.headers.get("if-none-match");
|
|
98
101
|
if (inm && inmMatches(inm, value)) {
|
|
99
102
|
const stripped = new Headers();
|
|
100
|
-
for (const allow of [
|
|
103
|
+
for (const allow of [
|
|
104
|
+
"cache-control",
|
|
105
|
+
"content-location",
|
|
106
|
+
"date",
|
|
107
|
+
"etag",
|
|
108
|
+
"expires",
|
|
109
|
+
"vary",
|
|
110
|
+
]) {
|
|
101
111
|
const v = headers.get(allow);
|
|
102
112
|
if (v !== null)
|
|
103
113
|
stripped.set(allow, v);
|
package/dist/geo-block.js
CHANGED
|
@@ -90,12 +90,10 @@ export function geoBlock(opts) {
|
|
|
90
90
|
const hasLookup = typeof opts.lookupCountry === "function";
|
|
91
91
|
const hasResolve = typeof opts.resolveCountry === "function";
|
|
92
92
|
if (hasLookup === hasResolve) {
|
|
93
|
-
throw new Error('geoBlock(): exactly one of "lookupCountry" or "resolveCountry" must ' +
|
|
94
|
-
"be provided.");
|
|
93
|
+
throw new Error('geoBlock(): exactly one of "lookupCountry" or "resolveCountry" must ' + "be provided.");
|
|
95
94
|
}
|
|
96
95
|
if (opts.mode !== undefined && opts.mode !== "block" && opts.mode !== "log") {
|
|
97
|
-
throw new Error(`geoBlock(): invalid mode ${JSON.stringify(opts.mode)}; expected ` +
|
|
98
|
-
'"block" or "log".');
|
|
96
|
+
throw new Error(`geoBlock(): invalid mode ${JSON.stringify(opts.mode)}; expected ` + '"block" or "log".');
|
|
99
97
|
}
|
|
100
98
|
const allow = new Set((opts.allow ?? []).map(normalizeConfiguredCode));
|
|
101
99
|
const deny = new Set((opts.deny ?? []).map(normalizeConfiguredCode));
|
|
@@ -107,8 +105,7 @@ export function geoBlock(opts) {
|
|
|
107
105
|
const onBlock = opts.onBlock;
|
|
108
106
|
const lookupCountry = opts.lookupCountry;
|
|
109
107
|
const resolveCountry = opts.resolveCountry;
|
|
110
|
-
const resolveIp = opts.resolveIp ??
|
|
111
|
-
(opts.trustProxyHeaders ? forwardedIpResolver : noIpResolver);
|
|
108
|
+
const resolveIp = opts.resolveIp ?? (opts.trustProxyHeaders ? forwardedIpResolver : noIpResolver);
|
|
112
109
|
return {
|
|
113
110
|
async beforeHandle(ctx) {
|
|
114
111
|
let ip;
|
|
@@ -120,9 +117,7 @@ export function geoBlock(opts) {
|
|
|
120
117
|
ip = resolveIp(ctx) ?? undefined;
|
|
121
118
|
rawCountry = ip ? await lookupCountry(ip) : undefined;
|
|
122
119
|
}
|
|
123
|
-
const country = rawCountry && rawCountry.trim()
|
|
124
|
-
? rawCountry.trim().toUpperCase()
|
|
125
|
-
: undefined;
|
|
120
|
+
const country = rawCountry && rawCountry.trim() ? rawCountry.trim().toUpperCase() : undefined;
|
|
126
121
|
let reason;
|
|
127
122
|
if (!country) {
|
|
128
123
|
if (!allowUnknown)
|
package/dist/hashing.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @since 0.15.0
|
|
24
24
|
*/
|
|
25
|
-
import { randomBytes, scrypt as scryptCb, timingSafeEqual as nodeTimingSafeEqual } from "node:crypto";
|
|
25
|
+
import { randomBytes, scrypt as scryptCb, timingSafeEqual as nodeTimingSafeEqual, } from "node:crypto";
|
|
26
26
|
// OWASP-aligned scrypt parameters (Password Storage Cheat Sheet, 2024).
|
|
27
27
|
const SCRYPT_N = 1 << 17; // 131072
|
|
28
28
|
const SCRYPT_R = 8;
|
package/dist/http-signatures.js
CHANGED
|
@@ -61,8 +61,7 @@ const ENC = new TextEncoder();
|
|
|
61
61
|
// WebCrypto + encoding helpers
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
function getCrypto() {
|
|
64
|
-
const c = globalThis
|
|
65
|
-
.crypto;
|
|
64
|
+
const c = globalThis.crypto;
|
|
66
65
|
if (!c?.subtle) {
|
|
67
66
|
throw new Error("http-signatures: WebCrypto SubtleCrypto API is unavailable on this runtime.");
|
|
68
67
|
}
|
|
@@ -181,9 +180,7 @@ async function importKey(alg, material, usage) {
|
|
|
181
180
|
return c.subtle.importKey("raw", material, spec.importParams, false, [usage]);
|
|
182
181
|
}
|
|
183
182
|
if (isJsonWebKey(material)) {
|
|
184
|
-
const key = await c.subtle.importKey("jwk", material, spec.importParams, false, [
|
|
185
|
-
usage,
|
|
186
|
-
]);
|
|
183
|
+
const key = await c.subtle.importKey("jwk", material, spec.importParams, false, [usage]);
|
|
187
184
|
assertRsaModulusFloor(alg, key);
|
|
188
185
|
return key;
|
|
189
186
|
}
|
|
@@ -686,9 +683,7 @@ export async function verifyMessage(opts) {
|
|
|
686
683
|
return fail("key_not_found");
|
|
687
684
|
let keyMaterial;
|
|
688
685
|
let pinnedAlg;
|
|
689
|
-
if (resolved instanceof Uint8Array ||
|
|
690
|
-
isCryptoKey(resolved) ||
|
|
691
|
-
isJsonWebKey(resolved)) {
|
|
686
|
+
if (resolved instanceof Uint8Array || isCryptoKey(resolved) || isJsonWebKey(resolved)) {
|
|
692
687
|
keyMaterial = resolved;
|
|
693
688
|
}
|
|
694
689
|
else {
|
package/dist/index.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export { session, rotateSession, signValue, verifySignedValue, MemorySessionStor
|
|
|
89
89
|
export type { SessionOptions, SessionCookieOptions, SessionContext, SessionRecord, SessionStore, SessionState, RotateSessionOptions, } from "./session.js";
|
|
90
90
|
export { idempotency, MemoryIdempotencyStore, _resetSharedIdempotencyStoresForTests, } from "./idempotency.js";
|
|
91
91
|
export type { IdempotencyOptions, IdempotencyStore, IdempotencyRecord, StoredIdempotentResponse, } from "./idempotency.js";
|
|
92
|
-
export { responseCache, MemoryResponseCacheStore, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
92
|
+
export { responseCache, MemoryResponseCacheStore, RESPONSE_CACHE_HOOK_MARKER, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
93
93
|
export type { ResponseCacheOptions, ResponseCacheStore, CachedResponse } from "./response-cache.js";
|
|
94
94
|
export { encodeCursor, decodeCursor, buildLinkHeader, buildPageLinks, paginationQuery, MAX_CURSOR_LENGTH, } from "./pagination.js";
|
|
95
95
|
export type { PaginationLink, PageLinkOptions, PageLinks, PaginationQueryOptions, PaginationParams, PaginationQuerySchema, } from "./pagination.js";
|
|
@@ -99,7 +99,7 @@ export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema,
|
|
|
99
99
|
export type { FileFieldSchema, FileFieldOptions, FileMagicBytesOption, FileMagicBytesSignature, MultipartObjectOptions, MultipartShape, UploadedFile, } from "./multipart.js";
|
|
100
100
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
|
101
101
|
export type { OtelTracingOptions, TracingAttributes, TracingAttributeValue, TracingSpan, TracingStartSpanOptions, TracingTracer, } from "./tracing.js";
|
|
102
|
-
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, } from "./tenancy.js";
|
|
102
|
+
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, TENANCY_HOOK_MARKER, TENANCY_RESOLVED_MARKER, TENANT_UNRESOLVED, } from "./tenancy.js";
|
|
103
103
|
export type { TenancyOptions, TenantResolver, TenantScopeOptions, SubdomainTenantOptions, PathPrefixTenantOptions, ClaimTenantOptions, UnresolvedStatus, InvalidStatus, } from "./tenancy.js";
|
|
104
104
|
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
|
105
105
|
export type { WebSocketConnection, WebSocketContext, WebSocketHandler, WebSocketMeta, WebSocketRouteEntry, NormalizedWebSocketOptions, WebSocketBeforeUpgrade, HandshakeResult, ParsedFrame, MessageEvent as WebSocketMessageEvent, FrameSinkEvents, } from "./websocket.js";
|
package/dist/index.js
CHANGED
|
@@ -45,10 +45,10 @@ export { httpBearerScheme, httpBasicScheme, apiKeyScheme, oauth2Scheme, openIdCo
|
|
|
45
45
|
export { discriminator, discriminatedUnion } from "./discriminator.js";
|
|
46
46
|
export { session, rotateSession, signValue, verifySignedValue, MemorySessionStore, SESSION_HOOK_MARKER, SESSION_SECRETS_MARKER, } from "./session.js";
|
|
47
47
|
export { idempotency, MemoryIdempotencyStore, _resetSharedIdempotencyStoresForTests, } from "./idempotency.js";
|
|
48
|
-
export { responseCache, MemoryResponseCacheStore, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
48
|
+
export { responseCache, MemoryResponseCacheStore, RESPONSE_CACHE_HOOK_MARKER, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
49
49
|
export { encodeCursor, decodeCursor, buildLinkHeader, buildPageLinks, paginationQuery, MAX_CURSOR_LENGTH, } from "./pagination.js";
|
|
50
50
|
export { MetricsRegistry, Counter, Gauge, Histogram, httpMetrics, DEFAULT_DURATION_BUCKETS, PROMETHEUS_CONTENT_TYPE, } from "./metrics.js";
|
|
51
51
|
export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema, } from "./multipart.js";
|
|
52
52
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
|
53
|
-
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, } from "./tenancy.js";
|
|
53
|
+
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, TENANCY_HOOK_MARKER, TENANCY_RESOLVED_MARKER, TENANT_UNRESOLVED, } from "./tenancy.js";
|
|
54
54
|
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
package/dist/ip-reputation.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import { ForbiddenError } from "./errors.js";
|
|
48
48
|
import { fetchGuard } from "./fetch-guard.js";
|
|
49
|
-
import { compileCidrMatcher, matchesMatcher, parseIp
|
|
49
|
+
import { compileCidrMatcher, matchesMatcher, parseIp } from "./ip-restriction.js";
|
|
50
50
|
const DEFAULT_REFRESH_MS = 60 * 60_000;
|
|
51
51
|
const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
52
52
|
const DEFAULT_MESSAGE = "IP address not permitted";
|