@daloyjs/core 1.0.0-rc.3 → 1.0.0-rc.5
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 +103 -41
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +41 -5
- package/dist/adapters/deno.js +24 -7
- package/dist/adapters/lambda.d.ts +59 -2
- package/dist/adapters/lambda.js +136 -20
- package/dist/adapters/node.d.ts +8 -1
- package/dist/adapters/node.js +104 -19
- package/dist/app.d.ts +131 -11
- package/dist/app.js +305 -217
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +64 -18
- package/dist/client.js +36 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +12 -3
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +13 -1
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/internal-response.d.ts +15 -0
- package/dist/internal-response.js +27 -0
- package/dist/jwk.d.ts +11 -7
- package/dist/jwk.js +11 -7
- package/dist/logger.d.ts +45 -0
- package/dist/logger.js +137 -0
- package/dist/mcp.js +21 -15
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +129 -43
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +8 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +9 -2
- package/dist/safe-redirect.js +29 -4
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +62 -0
- package/dist/security.js +220 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/tenancy.d.ts +2 -2
- package/dist/time-claims.js +3 -1
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/dist/waf.js +86 -26
- package/package.json +11 -4
package/dist/jwk.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JwtAlgorithm } from "./jwt.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Hooks, PreBodyContext } from "./types.js";
|
|
3
3
|
/** Asymmetric algorithms accepted by {@link jwk}. */
|
|
4
4
|
export type JwkAlgorithm = Exclude<JwtAlgorithm, "HS256" | "HS384" | "HS512">;
|
|
5
5
|
/** Minimal JWKS document shape (RFC 7517 §5). */
|
|
@@ -12,8 +12,12 @@ export interface JwkSet {
|
|
|
12
12
|
* `https://` URL (fetched with TTL caching), or a custom async resolver.
|
|
13
13
|
*/
|
|
14
14
|
export type JwkSource = JwkSet | string | (() => JwkSet | Promise<JwkSet>);
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Per-request payload-revalidation hook run before request-body I/O.
|
|
17
|
+
* The supplied {@link PreBodyContext} exposes raw headers/query/params and an
|
|
18
|
+
* always-`undefined` body.
|
|
19
|
+
*/
|
|
20
|
+
export type JwkVerifyHook = (payload: Record<string, unknown>, ctx: PreBodyContext<any>) => boolean | void | Promise<boolean | void>;
|
|
17
21
|
/**
|
|
18
22
|
* Options for {@link jwk}: the JWKS source and asymmetric algorithm allowlist
|
|
19
23
|
* are required; issuer / audience / clock-skew checks, JWKS fetch caching,
|
|
@@ -61,10 +65,10 @@ export interface JwkOptions {
|
|
|
61
65
|
*/
|
|
62
66
|
fetch?: typeof fetch;
|
|
63
67
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
* Optional per-request revalidation hook. Returning `false` rejects the
|
|
69
|
+
* request with `403`; returning `true` or `undefined` accepts. Use for
|
|
70
|
+
* revocation lists / token-version counters / "user changed password since
|
|
71
|
+
* this JWT was issued". Runs before request-body I/O.
|
|
68
72
|
*
|
|
69
73
|
* @since 0.22.0
|
|
70
74
|
*/
|
package/dist/jwk.js
CHANGED
|
@@ -30,9 +30,15 @@
|
|
|
30
30
|
import { ForbiddenError } from "./errors.js";
|
|
31
31
|
import { createJwtVerifier, JwtError, } from "./jwt.js";
|
|
32
32
|
const ALLOWED_JWK_ALGS = new Set([
|
|
33
|
-
"RS256",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
33
|
+
"RS256",
|
|
34
|
+
"RS384",
|
|
35
|
+
"RS512",
|
|
36
|
+
"PS256",
|
|
37
|
+
"PS384",
|
|
38
|
+
"PS512",
|
|
39
|
+
"ES256",
|
|
40
|
+
"ES384",
|
|
41
|
+
"ES512",
|
|
36
42
|
"EdDSA",
|
|
37
43
|
]);
|
|
38
44
|
function unauthorized(realm, errorCode, description) {
|
|
@@ -59,9 +65,7 @@ function sanitizeAuthParam(value) {
|
|
|
59
65
|
return value.replace(/[\u0000-\u001f\u007f"\\]/g, "");
|
|
60
66
|
}
|
|
61
67
|
function isJwkSet(value) {
|
|
62
|
-
return (typeof value === "object" &&
|
|
63
|
-
value !== null &&
|
|
64
|
-
Array.isArray(value.keys));
|
|
68
|
+
return (typeof value === "object" && value !== null && Array.isArray(value.keys));
|
|
65
69
|
}
|
|
66
70
|
function findJwkByKid(jwks, kid) {
|
|
67
71
|
for (const k of jwks.keys) {
|
|
@@ -236,7 +240,7 @@ export function jwk(opts) {
|
|
|
236
240
|
return cachedVerifier;
|
|
237
241
|
}
|
|
238
242
|
const authHooks = {
|
|
239
|
-
async
|
|
243
|
+
async preBody(ctx) {
|
|
240
244
|
const header = ctx.request.headers.get("authorization") ?? "";
|
|
241
245
|
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
242
246
|
if (!match) {
|
package/dist/logger.d.ts
CHANGED
|
@@ -133,4 +133,49 @@ export declare function createLogger(opts?: ConsoleLoggerOptions): Logger;
|
|
|
133
133
|
* @since 0.1.0
|
|
134
134
|
*/
|
|
135
135
|
export declare const noopLogger: Logger;
|
|
136
|
+
/**
|
|
137
|
+
* Query parameter names whose values are redacted when a request URL is
|
|
138
|
+
* bound into a log record. Case-insensitive. Covers OAuth redirect params,
|
|
139
|
+
* API keys in query strings, signed-URL tokens, session identifiers, and the
|
|
140
|
+
* exact-named parameters of AWS SigV4 / GCS V4 presigned URLs (the `x-amz-*`
|
|
141
|
+
* and `x-goog-*` families are additionally matched by prefix — see
|
|
142
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}).
|
|
143
|
+
*
|
|
144
|
+
* @since 1.0.0
|
|
145
|
+
*/
|
|
146
|
+
export declare const SENSITIVE_URL_QUERY_KEYS: readonly string[];
|
|
147
|
+
/**
|
|
148
|
+
* Case-insensitive query-key prefixes whose values are always redacted in a
|
|
149
|
+
* logged URL. Covers the full AWS SigV4 (`X-Amz-*`) and GCS V4 (`X-Goog-*`)
|
|
150
|
+
* presigned-URL parameter families so a signature never leaks even if a
|
|
151
|
+
* provider adds a new signed parameter name. Redacting the non-secret members
|
|
152
|
+
* of the bundle (`X-Amz-Date`, `X-Amz-Expires`, …) is harmless in a log line.
|
|
153
|
+
*
|
|
154
|
+
* @since 1.0.0
|
|
155
|
+
*/
|
|
156
|
+
export declare const SENSITIVE_URL_QUERY_KEY_PREFIXES: readonly string[];
|
|
157
|
+
/**
|
|
158
|
+
* Produce a log-safe form of a request URL.
|
|
159
|
+
*
|
|
160
|
+
* Keeps scheme, host, and path for operability. Redacts values of
|
|
161
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} / {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}
|
|
162
|
+
* (and JWT-like / credential-like query values) so OAuth `?code=`,
|
|
163
|
+
* `?access_token=`, and presigned-URL signatures (`?X-Amz-Signature=`,
|
|
164
|
+
* `?X-Goog-Signature=`) never land in durable error logs under the field name
|
|
165
|
+
* `url` (which the structured redactor does not rename-match).
|
|
166
|
+
*
|
|
167
|
+
* Malformed URLs fall back to the path-only prefix before `?` / `#`.
|
|
168
|
+
*
|
|
169
|
+
* This runs once per request on the logging path, so it fast-paths the common
|
|
170
|
+
* case: a URL with no query, no fragment, and no userinfo (`@`) delimiter is
|
|
171
|
+
* already log-safe and is returned verbatim without the WHATWG URL parse (about
|
|
172
|
+
* an order of magnitude cheaper). The `@` guard preserves userinfo stripping
|
|
173
|
+
* for the rare inputs that carry credentials in the authority — `request.url`
|
|
174
|
+
* itself never does, but this is a public utility.
|
|
175
|
+
*
|
|
176
|
+
* @param url - Absolute or relative request URL (typically `request.url`).
|
|
177
|
+
* @returns A string safe to attach as a logger binding.
|
|
178
|
+
* @since 1.0.0
|
|
179
|
+
*/
|
|
180
|
+
export declare function sanitizeUrlForLog(url: string): string;
|
|
136
181
|
export {};
|
package/dist/logger.js
CHANGED
|
@@ -34,6 +34,17 @@ export const DEFAULT_REDACT_KEYS = Object.freeze([
|
|
|
34
34
|
"refresh_token",
|
|
35
35
|
"id_token",
|
|
36
36
|
"client_secret",
|
|
37
|
+
// A structured field literally named `private_key` is always a secret and
|
|
38
|
+
// has a negligible false-positive rate as a log-field name (mirrors the
|
|
39
|
+
// existing `client_secret`). The broader OAuth/session query-string names
|
|
40
|
+
// (`code`, `state`, `id`, `key`, `sid`, `session`, `signature`, `sig`,
|
|
41
|
+
// `auth`, …) are DELIBERATELY NOT here: those are extremely common,
|
|
42
|
+
// non-secret structured field names (record ids, sort keys, UI state) and
|
|
43
|
+
// redacting them at every depth would corrupt normal operational logs.
|
|
44
|
+
// Secrets that ride in a *URL query string* are handled instead by
|
|
45
|
+
// {@link sanitizeUrlForLog} / {@link SENSITIVE_URL_QUERY_KEYS}, which only
|
|
46
|
+
// applies to the `url` field where the query context makes them sensitive.
|
|
47
|
+
"private_key",
|
|
37
48
|
// AI / LLM provider credential headers and body fields. Added in response
|
|
38
49
|
// to the LiteLLM 2026 "AI blast radius" incident class (Snyk 2026,
|
|
39
50
|
// CVE-2026-42208 + CVE-2026-33634) — an AI gateway that brokers prompts
|
|
@@ -265,3 +276,129 @@ export const noopLogger = {
|
|
|
265
276
|
return noopLogger;
|
|
266
277
|
},
|
|
267
278
|
};
|
|
279
|
+
/**
|
|
280
|
+
* Query parameter names whose values are redacted when a request URL is
|
|
281
|
+
* bound into a log record. Case-insensitive. Covers OAuth redirect params,
|
|
282
|
+
* API keys in query strings, signed-URL tokens, session identifiers, and the
|
|
283
|
+
* exact-named parameters of AWS SigV4 / GCS V4 presigned URLs (the `x-amz-*`
|
|
284
|
+
* and `x-goog-*` families are additionally matched by prefix — see
|
|
285
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}).
|
|
286
|
+
*
|
|
287
|
+
* @since 1.0.0
|
|
288
|
+
*/
|
|
289
|
+
export const SENSITIVE_URL_QUERY_KEYS = Object.freeze([
|
|
290
|
+
"authorization",
|
|
291
|
+
"access_token",
|
|
292
|
+
"refresh_token",
|
|
293
|
+
"id_token",
|
|
294
|
+
"token",
|
|
295
|
+
"api_key",
|
|
296
|
+
"apikey",
|
|
297
|
+
"api-key",
|
|
298
|
+
"key",
|
|
299
|
+
"password",
|
|
300
|
+
"passwd",
|
|
301
|
+
"secret",
|
|
302
|
+
"client_secret",
|
|
303
|
+
"code",
|
|
304
|
+
"state",
|
|
305
|
+
"session_state",
|
|
306
|
+
"session",
|
|
307
|
+
"sid",
|
|
308
|
+
"signature",
|
|
309
|
+
"sig",
|
|
310
|
+
"auth",
|
|
311
|
+
"private_key",
|
|
312
|
+
"x-api-key",
|
|
313
|
+
// AWS SigV4 presigned URL parameters. `X-Amz-Signature` is the secret; the
|
|
314
|
+
// credential (embeds the access-key id) and session token are equally
|
|
315
|
+
// sensitive. Also covered by the `x-amz-` prefix below.
|
|
316
|
+
"x-amz-signature",
|
|
317
|
+
"x-amz-credential",
|
|
318
|
+
"x-amz-security-token",
|
|
319
|
+
// Google Cloud Storage V4 signed URL parameters. Also covered by `x-goog-`.
|
|
320
|
+
"x-goog-signature",
|
|
321
|
+
"x-goog-credential",
|
|
322
|
+
"googleaccessid",
|
|
323
|
+
]);
|
|
324
|
+
/**
|
|
325
|
+
* Case-insensitive query-key prefixes whose values are always redacted in a
|
|
326
|
+
* logged URL. Covers the full AWS SigV4 (`X-Amz-*`) and GCS V4 (`X-Goog-*`)
|
|
327
|
+
* presigned-URL parameter families so a signature never leaks even if a
|
|
328
|
+
* provider adds a new signed parameter name. Redacting the non-secret members
|
|
329
|
+
* of the bundle (`X-Amz-Date`, `X-Amz-Expires`, …) is harmless in a log line.
|
|
330
|
+
*
|
|
331
|
+
* @since 1.0.0
|
|
332
|
+
*/
|
|
333
|
+
export const SENSITIVE_URL_QUERY_KEY_PREFIXES = Object.freeze([
|
|
334
|
+
"x-amz-",
|
|
335
|
+
"x-goog-",
|
|
336
|
+
]);
|
|
337
|
+
const SENSITIVE_URL_QUERY_KEY_SET = new Set(SENSITIVE_URL_QUERY_KEYS.map((k) => k.toLowerCase()));
|
|
338
|
+
/**
|
|
339
|
+
* Whether a URL query-parameter name is treated as secret-bearing when a
|
|
340
|
+
* request URL is bound into a log record. True when the lower-cased name is in
|
|
341
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} or starts with a
|
|
342
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES} entry.
|
|
343
|
+
*
|
|
344
|
+
* @param lowerKey - Already-lower-cased query-parameter name.
|
|
345
|
+
* @returns `true` if the value should be redacted.
|
|
346
|
+
*/
|
|
347
|
+
function isSensitiveUrlQueryKey(lowerKey) {
|
|
348
|
+
if (SENSITIVE_URL_QUERY_KEY_SET.has(lowerKey))
|
|
349
|
+
return true;
|
|
350
|
+
for (const prefix of SENSITIVE_URL_QUERY_KEY_PREFIXES) {
|
|
351
|
+
if (lowerKey.startsWith(prefix))
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Produce a log-safe form of a request URL.
|
|
358
|
+
*
|
|
359
|
+
* Keeps scheme, host, and path for operability. Redacts values of
|
|
360
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} / {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}
|
|
361
|
+
* (and JWT-like / credential-like query values) so OAuth `?code=`,
|
|
362
|
+
* `?access_token=`, and presigned-URL signatures (`?X-Amz-Signature=`,
|
|
363
|
+
* `?X-Goog-Signature=`) never land in durable error logs under the field name
|
|
364
|
+
* `url` (which the structured redactor does not rename-match).
|
|
365
|
+
*
|
|
366
|
+
* Malformed URLs fall back to the path-only prefix before `?` / `#`.
|
|
367
|
+
*
|
|
368
|
+
* This runs once per request on the logging path, so it fast-paths the common
|
|
369
|
+
* case: a URL with no query, no fragment, and no userinfo (`@`) delimiter is
|
|
370
|
+
* already log-safe and is returned verbatim without the WHATWG URL parse (about
|
|
371
|
+
* an order of magnitude cheaper). The `@` guard preserves userinfo stripping
|
|
372
|
+
* for the rare inputs that carry credentials in the authority — `request.url`
|
|
373
|
+
* itself never does, but this is a public utility.
|
|
374
|
+
*
|
|
375
|
+
* @param url - Absolute or relative request URL (typically `request.url`).
|
|
376
|
+
* @returns A string safe to attach as a logger binding.
|
|
377
|
+
* @since 1.0.0
|
|
378
|
+
*/
|
|
379
|
+
export function sanitizeUrlForLog(url) {
|
|
380
|
+
if (url.indexOf("?") === -1 &&
|
|
381
|
+
url.indexOf("#") === -1 &&
|
|
382
|
+
url.indexOf("@") === -1) {
|
|
383
|
+
return url;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const parsed = new URL(url);
|
|
387
|
+
if (parsed.search === "" && parsed.hash === "") {
|
|
388
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
389
|
+
}
|
|
390
|
+
const safe = new URL(parsed.origin + parsed.pathname);
|
|
391
|
+
for (const [key, value] of parsed.searchParams) {
|
|
392
|
+
const lower = key.toLowerCase();
|
|
393
|
+
const sensitiveKey = isSensitiveUrlQueryKey(lower);
|
|
394
|
+
const sensitiveValue = JWT_LIKE_RE.test(value) || CREDENTIAL_LIKE_RE.test(value);
|
|
395
|
+
CREDENTIAL_LIKE_RE.lastIndex = 0;
|
|
396
|
+
safe.searchParams.append(key, sensitiveKey || sensitiveValue ? "[REDACTED]" : value);
|
|
397
|
+
}
|
|
398
|
+
return safe.toString();
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
const cut = url.search(/[?#]/);
|
|
402
|
+
return cut === -1 ? url : url.slice(0, cut);
|
|
403
|
+
}
|
|
404
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeJsonParseLimited } from "./security.js";
|
|
2
2
|
/**
|
|
3
3
|
* Latest MCP protocol version DaloyJS negotiates by default.
|
|
4
4
|
*
|
|
@@ -355,10 +355,16 @@ function compileUriTemplate(template) {
|
|
|
355
355
|
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
356
356
|
/**
|
|
357
357
|
* Streamable HTTP DNS-rebinding defense: decide whether a browser `Origin`
|
|
358
|
-
* may talk to this MCP endpoint.
|
|
359
|
-
*
|
|
358
|
+
* may talk to this MCP endpoint.
|
|
359
|
+
*
|
|
360
|
+
* Loopback origins (`localhost` / `127.0.0.1` / `[::1]` / `*.localhost`) are
|
|
361
|
+
* allowed for local development. Every non-loopback origin must appear in
|
|
362
|
+
* the configured allowlist. We deliberately do **not** treat
|
|
363
|
+
* `Origin.host === request Host` as sufficient: under DNS rebinding both
|
|
364
|
+
* can be the attacker hostname resolving to the target IP, which would
|
|
365
|
+
* silently bypass an implicit same-origin check.
|
|
360
366
|
*/
|
|
361
|
-
function isAllowedOrigin(origin,
|
|
367
|
+
function isAllowedOrigin(origin, _request, allowlist) {
|
|
362
368
|
const normalized = origin.toLowerCase();
|
|
363
369
|
if (allowlist.has(normalized))
|
|
364
370
|
return true;
|
|
@@ -374,12 +380,7 @@ function isAllowedOrigin(origin, request, allowlist) {
|
|
|
374
380
|
const hostname = parsed.hostname;
|
|
375
381
|
if (LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost"))
|
|
376
382
|
return true;
|
|
377
|
-
|
|
378
|
-
return parsed.host === new URL(request.url).host;
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
+
return false;
|
|
383
384
|
}
|
|
384
385
|
/**
|
|
385
386
|
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
@@ -693,11 +694,10 @@ export function createMcpHandler(options) {
|
|
|
693
694
|
}
|
|
694
695
|
let message;
|
|
695
696
|
try {
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
message = safeJsonParse(raw);
|
|
697
|
+
// Use the limited parser (proto stripping + key/depth bounds) so an
|
|
698
|
+
// untrusted MCP client cannot DoS us with wide or deeply-nested JSON-RPC
|
|
699
|
+
// payloads, even within the MCP body cap. Matches the REST body parsers.
|
|
700
|
+
message = safeJsonParseLimited(raw);
|
|
701
701
|
}
|
|
702
702
|
catch {
|
|
703
703
|
return rpcError(null, PARSE_ERROR, "Invalid JSON in request body.", undefined, 400, headers);
|
|
@@ -789,6 +789,10 @@ export function mcpRoutes(path, handler, options = {}) {
|
|
|
789
789
|
path,
|
|
790
790
|
operationId: "mcpPost",
|
|
791
791
|
summary: "MCP Streamable HTTP endpoint",
|
|
792
|
+
// The transport handler owns JSON-RPC serialization and may also emit
|
|
793
|
+
// empty/streaming responses, so its web-standard Response is
|
|
794
|
+
// intentionally opaque to Daloy's response serializer.
|
|
795
|
+
acknowledgeNoResponseBodySchema: true,
|
|
792
796
|
responses,
|
|
793
797
|
handler: ({ request }) => handler(request),
|
|
794
798
|
},
|
|
@@ -797,6 +801,7 @@ export function mcpRoutes(path, handler, options = {}) {
|
|
|
797
801
|
path,
|
|
798
802
|
operationId: "mcpGet",
|
|
799
803
|
summary: "MCP Streamable HTTP discovery hint",
|
|
804
|
+
acknowledgeNoResponseBodySchema: true,
|
|
800
805
|
responses,
|
|
801
806
|
handler: ({ request }) => handler(request),
|
|
802
807
|
},
|
|
@@ -805,6 +810,7 @@ export function mcpRoutes(path, handler, options = {}) {
|
|
|
805
810
|
path,
|
|
806
811
|
operationId: "mcpOptions",
|
|
807
812
|
summary: "MCP Streamable HTTP preflight",
|
|
813
|
+
acknowledgeNoResponseBodySchema: true,
|
|
808
814
|
responses,
|
|
809
815
|
handler: ({ request }) => handler(request),
|
|
810
816
|
},
|
package/dist/middleware.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* All middlewares return `Hooks` objects so they compose with `app.use(...)`,
|
|
5
5
|
* groups, and per-route hooks identically.
|
|
6
6
|
*/
|
|
7
|
-
import type { Hooks, BaseContext } from "./types.js";
|
|
7
|
+
import type { Hooks, BaseContext, PreBodyContext } from "./types.js";
|
|
8
8
|
import { timingSafeEqual } from "./security.js";
|
|
9
9
|
/** Options for {@link requestId}. */
|
|
10
10
|
export interface RequestIdOptions {
|
|
@@ -327,6 +327,23 @@ export declare const CSRF_HOOK_MARKER: unique symbol;
|
|
|
327
327
|
* @since 1.0.0
|
|
328
328
|
*/
|
|
329
329
|
export declare const AUTH_HOOK_MARKER: unique symbol;
|
|
330
|
+
/**
|
|
331
|
+
* Internal list of request-budget hooks that must run when a later `preBody`
|
|
332
|
+
* guard rejects. This preserves registration order for `rateLimit()` before
|
|
333
|
+
* authentication without moving body-aware middleware ahead of validation.
|
|
334
|
+
*
|
|
335
|
+
* @internal
|
|
336
|
+
*/
|
|
337
|
+
export declare const EARLY_REJECTION_HOOK_MARKER: unique symbol;
|
|
338
|
+
/**
|
|
339
|
+
* Compose `preBody` hooks while honoring request-budget hooks registered
|
|
340
|
+
* before the guard that rejects. Used internally by App and hook combinators.
|
|
341
|
+
*
|
|
342
|
+
* @param layers - Hook layers in registration order.
|
|
343
|
+
* @returns A composed `preBody` hook, or `undefined` when no layer has one.
|
|
344
|
+
* @internal
|
|
345
|
+
*/
|
|
346
|
+
export declare function _mergePreBodyWithEarlyRejections(layers: Hooks[]): NonNullable<Hooks["preBody"]> | undefined;
|
|
330
347
|
/**
|
|
331
348
|
* Mark a custom {@link Hooks} bundle as performing request authentication.
|
|
332
349
|
*
|
|
@@ -343,7 +360,7 @@ export declare const AUTH_HOOK_MARKER: unique symbol;
|
|
|
343
360
|
* @example
|
|
344
361
|
* ```ts
|
|
345
362
|
* app.use(markAuthHook({
|
|
346
|
-
* async
|
|
363
|
+
* async preBody(ctx) {
|
|
347
364
|
* if (!(await myVerify(ctx.request))) throw new UnauthorizedError();
|
|
348
365
|
* },
|
|
349
366
|
* }));
|
|
@@ -426,14 +443,27 @@ export interface RateLimitStore {
|
|
|
426
443
|
resetMs: number;
|
|
427
444
|
}>;
|
|
428
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Context accepted by rate-limit key generators. It is validated on the
|
|
448
|
+
* ordinary path, but may be a pre-body context when a limiter registered
|
|
449
|
+
* before authentication counts an early credential rejection.
|
|
450
|
+
*
|
|
451
|
+
* @since 1.0.0
|
|
452
|
+
*/
|
|
453
|
+
export type RateLimitContext = PreBodyContext<any> | BaseContext<any, any>;
|
|
429
454
|
/** Options for {@link rateLimit}. */
|
|
430
455
|
export interface RateLimitOptions {
|
|
431
456
|
/** Rolling-window width in milliseconds (e.g. `60_000` for one minute). */
|
|
432
457
|
windowMs: number;
|
|
433
458
|
/** Maximum allowed requests per `windowMs` per key. */
|
|
434
459
|
max: number;
|
|
435
|
-
/**
|
|
436
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Derive the bucket key from `ctx`. Default returns `"global"`. When the
|
|
462
|
+
* limiter precedes `preBody` authentication, it also receives failed
|
|
463
|
+
* attempts before body I/O; rely only on the raw request and state populated
|
|
464
|
+
* by earlier `preBody` hooks.
|
|
465
|
+
*/
|
|
466
|
+
keyGenerator?: (ctx: RateLimitContext) => string;
|
|
437
467
|
/** Custom backend (default: shared in-memory store). */
|
|
438
468
|
store?: RateLimitStore;
|
|
439
469
|
/**
|
|
@@ -483,6 +513,11 @@ export declare function _resetSharedRateLimitStoresForTests(): void;
|
|
|
483
513
|
* `X-Forwarded-For` / `X-Real-IP` when behind a trusted proxy, or supply a
|
|
484
514
|
* custom `keyGenerator` (e.g. derive from the authenticated user id).
|
|
485
515
|
*
|
|
516
|
+
* Registration order remains security-significant: a limiter placed before a
|
|
517
|
+
* `preBody` auth hook counts rejected credentials and can replace the later
|
|
518
|
+
* `401` with `429`, without consuming the request body. On ordinary accepted
|
|
519
|
+
* requests it retains the validated `beforeHandle` timing.
|
|
520
|
+
*
|
|
486
521
|
* @example
|
|
487
522
|
* ```ts
|
|
488
523
|
* import { rateLimit } from "@daloyjs/core";
|
|
@@ -508,7 +543,7 @@ export interface LoginThrottleOptions {
|
|
|
508
543
|
/** Shared bucket id. Default: `"login"`. */
|
|
509
544
|
groupId?: string;
|
|
510
545
|
/** Derive the caller key. Defaults to trusted proxy headers only when enabled. */
|
|
511
|
-
keyGenerator?: (ctx:
|
|
546
|
+
keyGenerator?: (ctx: RateLimitContext) => string;
|
|
512
547
|
/** Shared store for the hard limit. Uses rateLimit()'s in-memory group bucket by default. */
|
|
513
548
|
store?: RateLimitStore;
|
|
514
549
|
/** Trust x-forwarded-for / x-real-ip when deriving the default key. Default: false. */
|
|
@@ -530,6 +565,8 @@ export interface LoginThrottleOptions {
|
|
|
530
565
|
* Mount the same `loginThrottle()` instance (or multiple instances with the
|
|
531
566
|
* same `groupId`) across related routes so an attacker cannot bypass the limit
|
|
532
567
|
* by rotating between password, OTP, and reset endpoints.
|
|
568
|
+
* When registered before `preBody` authentication it counts and progressively
|
|
569
|
+
* delays rejected credentials without consuming a declared request body.
|
|
533
570
|
*
|
|
534
571
|
* @param opts - Throttle tuning (see {@link LoginThrottleOptions}); every field has a safe default.
|
|
535
572
|
* @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`.
|
|
@@ -588,7 +625,7 @@ export declare function timing(headerName?: string): Hooks;
|
|
|
588
625
|
*
|
|
589
626
|
* @since 0.22.0
|
|
590
627
|
*/
|
|
591
|
-
export type BearerAuthVerifyHook<TCredentials = string> = (credentials: TCredentials, ctx:
|
|
628
|
+
export type BearerAuthVerifyHook<TCredentials = string> = (credentials: TCredentials, ctx: PreBodyContext<any>) => boolean | void | Promise<boolean | void>;
|
|
592
629
|
/** Options for {@link bearerAuth}. */
|
|
593
630
|
export interface BearerAuthOptions {
|
|
594
631
|
/** Cheap, stateless token check (signature / format). */
|
|
@@ -616,6 +653,8 @@ export interface BearerAuthOptions {
|
|
|
616
653
|
* optional `verify` hook is the integration point for revocation
|
|
617
654
|
* lists, token-version counters, and other per-request invalidation checks
|
|
618
655
|
* that `validate` cannot answer statelessly.
|
|
656
|
+
* Both checks run before request-body I/O; `verify` receives a
|
|
657
|
+
* {@link PreBodyContext} whose `body` is always `undefined`.
|
|
619
658
|
*
|
|
620
659
|
* @example
|
|
621
660
|
* ```ts
|
|
@@ -757,13 +796,15 @@ export interface BasicAuthOptions {
|
|
|
757
796
|
* after the framework has stamped `ctx.state.user`. Use this to decorate
|
|
758
797
|
* `ctx.state` with extra fields (typed via `AppState`) so handlers do not
|
|
759
798
|
* re-parse the `Authorization` header in every route.
|
|
799
|
+
* Runs before request-body I/O with a {@link PreBodyContext}; `ctx.body` is
|
|
800
|
+
* always `undefined`.
|
|
760
801
|
*
|
|
761
802
|
* @since 0.22.0
|
|
762
803
|
*/
|
|
763
804
|
onAuthSuccess?: (creds: {
|
|
764
805
|
username: string;
|
|
765
806
|
password: string;
|
|
766
|
-
}, ctx:
|
|
807
|
+
}, ctx: PreBodyContext<any>) => void | Promise<void>;
|
|
767
808
|
}
|
|
768
809
|
/**
|
|
769
810
|
* HTTP Basic Authentication middleware (RFC 7617).
|