@daloyjs/core 1.0.0-beta.7 → 1.0.0-rc.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 +7 -5
- package/dist/adapters/node.js +216 -11
- package/dist/app.d.ts +32 -8
- package/dist/app.js +418 -67
- package/dist/http-signatures.js +44 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/ip-reputation.d.ts +8 -2
- package/dist/ip-reputation.js +7 -1
- package/dist/jwk.js +6 -1
- package/dist/mcp.d.ts +80 -9
- package/dist/mcp.js +206 -4
- package/dist/middleware.d.ts +38 -0
- package/dist/middleware.js +45 -4
- package/dist/mtls.js +6 -1
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/package.json +1 -1
package/dist/http-signatures.js
CHANGED
|
@@ -48,6 +48,14 @@ export const DEFAULT_SIGNATURE_CLOCK_SKEW_SECONDS = 60;
|
|
|
48
48
|
const MAX_HEADER_LENGTH = 8192;
|
|
49
49
|
/** Minimum byte length for a raw HMAC secret (RFC 7518 §3.2). */
|
|
50
50
|
const MIN_HMAC_KEY_BYTES = 32;
|
|
51
|
+
/**
|
|
52
|
+
* Minimum RSA modulus size accepted for `rsa-*` signature algorithms. NIST
|
|
53
|
+
* SP 800-131A has disallowed RSA keys shorter than 2048 bits since 2014; the
|
|
54
|
+
* JWT verifier ({@link file://./jwt.ts}) enforces the same floor, so the HTTP
|
|
55
|
+
* Message Signatures path holds the parity to keep undersized (crackable) RSA
|
|
56
|
+
* keys out of every signature-verification surface in the framework.
|
|
57
|
+
*/
|
|
58
|
+
const MIN_RSA_KEY_BITS = 2048;
|
|
51
59
|
const ENC = new TextEncoder();
|
|
52
60
|
// ---------------------------------------------------------------------------
|
|
53
61
|
// WebCrypto + encoding helpers
|
|
@@ -132,11 +140,37 @@ function algSpec(alg) {
|
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Refuse RSA keys whose modulus is shorter than {@link MIN_RSA_KEY_BITS}.
|
|
145
|
+
*
|
|
146
|
+
* Only applies to the `rsa-*` algorithms — non-RSA keys are ignored. Every RSA
|
|
147
|
+
* `CryptoKey` carries a numeric `algorithm.modulusLength`; when WebCrypto
|
|
148
|
+
* reports a length below the floor the key is refused for both signing and
|
|
149
|
+
* verification. Mirrors the JWT verifier's `assertRsaModulusFloor` so no
|
|
150
|
+
* signature surface in the framework accepts an undersized RSA key.
|
|
151
|
+
*
|
|
152
|
+
* @param alg - The HTTP signature algorithm the key will be used with.
|
|
153
|
+
* @param key - The imported (or caller-supplied) `CryptoKey`.
|
|
154
|
+
* @throws {TypeError} When `alg` is RSA and the modulus is under the floor.
|
|
155
|
+
*/
|
|
156
|
+
function assertRsaModulusFloor(alg, key) {
|
|
157
|
+
if (alg !== "rsa-pss-sha512" && alg !== "rsa-v1_5-sha256")
|
|
158
|
+
return;
|
|
159
|
+
const algorithm = key.algorithm;
|
|
160
|
+
const modulusLength = algorithm?.modulusLength;
|
|
161
|
+
if (typeof modulusLength !== "number" || !Number.isFinite(modulusLength))
|
|
162
|
+
return;
|
|
163
|
+
if (modulusLength < MIN_RSA_KEY_BITS) {
|
|
164
|
+
throw new TypeError(`http-signatures: ${alg} key modulus must be at least ${MIN_RSA_KEY_BITS} bits (NIST SP 800-131A); got ${modulusLength}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
135
167
|
async function importKey(alg, material, usage) {
|
|
136
168
|
const spec = algSpec(alg);
|
|
137
169
|
const c = getCrypto();
|
|
138
|
-
if (isCryptoKey(material))
|
|
170
|
+
if (isCryptoKey(material)) {
|
|
171
|
+
assertRsaModulusFloor(alg, material);
|
|
139
172
|
return material;
|
|
173
|
+
}
|
|
140
174
|
if (material instanceof Uint8Array) {
|
|
141
175
|
if (!spec.symmetric) {
|
|
142
176
|
throw new TypeError(`http-signatures: raw byte keys are only supported for hmac-sha256; got ${alg}.`);
|
|
@@ -147,9 +181,11 @@ async function importKey(alg, material, usage) {
|
|
|
147
181
|
return c.subtle.importKey("raw", material, spec.importParams, false, [usage]);
|
|
148
182
|
}
|
|
149
183
|
if (isJsonWebKey(material)) {
|
|
150
|
-
|
|
184
|
+
const key = await c.subtle.importKey("jwk", material, spec.importParams, false, [
|
|
151
185
|
usage,
|
|
152
186
|
]);
|
|
187
|
+
assertRsaModulusFloor(alg, key);
|
|
188
|
+
return key;
|
|
153
189
|
}
|
|
154
190
|
throw new TypeError("http-signatures: unsupported key material.");
|
|
155
191
|
}
|
|
@@ -731,7 +767,7 @@ export function verifyRequest(request, opts) {
|
|
|
731
767
|
export function httpSignatureAuth(opts) {
|
|
732
768
|
const stateKey = opts.stateKey ?? "httpSignature";
|
|
733
769
|
const message = opts.message ?? "Valid HTTP message signature required";
|
|
734
|
-
|
|
770
|
+
const authHooks = {
|
|
735
771
|
async beforeHandle(ctx) {
|
|
736
772
|
const headers = ctx.request.headers;
|
|
737
773
|
if (opts.optional && !headers.has("signature"))
|
|
@@ -744,6 +780,11 @@ export function httpSignatureAuth(opts) {
|
|
|
744
780
|
return undefined;
|
|
745
781
|
},
|
|
746
782
|
};
|
|
783
|
+
// Same global symbol as middleware's AUTH_HOOK_MARKER (stamped inline to keep
|
|
784
|
+
// the middleware module out of this bundle): lets the route-auth boot guard
|
|
785
|
+
// recognize that a route declaring `auth:` is actually enforced here.
|
|
786
|
+
authHooks[Symbol.for("daloyjs.auth.hook")] = true;
|
|
787
|
+
return authHooks;
|
|
747
788
|
}
|
|
748
789
|
const CONTENT_DIGEST_HASH = {
|
|
749
790
|
"sha-256": "SHA-256",
|
package/dist/index.d.ts
CHANGED
|
@@ -19,11 +19,11 @@ export type { StandardSchemaV1 } from "./schema.js";
|
|
|
19
19
|
export { validate, isStandardSchema } from "./schema.js";
|
|
20
20
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
21
21
|
export type { ChangeSeverity, OpenAPIChange, OpenAPIDiffResult } from "./openapi-diff.js";
|
|
22
|
-
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, } from "./mcp.js";
|
|
23
|
-
export type { McpContent, McpEmbeddedResourceContent, McpHandler, McpHandlerOptions, McpIcon, McpImageContent, McpJsonObject, McpJsonRpcId, McpJsonSchema, McpJsonValue, McpPrompt, McpPromptArgument, McpPromptDefinition, McpPromptMessage, McpPromptResult, McpRequestContext, McpResource, McpResourceContents, McpResourceDefinition, McpResourceTemplate, McpResourceTemplateDefinition, McpServerInfo, McpTextContent, McpTool, McpToolAnnotations, McpToolHandler, McpToolResult, } from "./mcp.js";
|
|
22
|
+
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, validateMcpInput, } from "./mcp.js";
|
|
23
|
+
export type { McpContent, McpEmbeddedResourceContent, McpHandler, McpHandlerOptions, McpIcon, McpImageContent, McpJsonObject, McpJsonRpcId, McpJsonSchema, McpJsonValue, McpPrompt, McpPromptArgument, McpPromptDefinition, McpPromptMessage, McpPromptResult, McpRequestContext, McpResource, McpResourceContents, McpResourceDefinition, McpResourceTemplate, McpResourceTemplateDefinition, McpRoutesOptions, McpServerInfo, McpTextContent, McpTool, McpToolAnnotations, McpToolHandler, McpToolResult, } from "./mcp.js";
|
|
24
24
|
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
25
25
|
export type { WebhookHmacAlgorithm } from "./security.js";
|
|
26
|
-
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
26
|
+
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, markAuthHook, AUTH_HOOK_MARKER, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
27
27
|
export { etag } from "./etag.js";
|
|
28
28
|
export type { ETagOptions } from "./etag.js";
|
|
29
29
|
export { compression, COMPRESSION_HOOK_MARKER, _resetCompressionRuntimeProbeForTests, } from "./compression.js";
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,9 @@ export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
|
11
11
|
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
12
12
|
export { validate, isStandardSchema } from "./schema.js";
|
|
13
13
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
14
|
-
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, } from "./mcp.js";
|
|
14
|
+
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, validateMcpInput, } from "./mcp.js";
|
|
15
15
|
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
16
|
-
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
16
|
+
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, markAuthHook, AUTH_HOOK_MARKER, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
17
17
|
export { etag } from "./etag.js";
|
|
18
18
|
export { compression, COMPRESSION_HOOK_MARKER, _resetCompressionRuntimeProbeForTests, } from "./compression.js";
|
|
19
19
|
export { createJwtSigner, createJwtVerifier, JwtError, DEFAULT_JWT_MAX_LIFETIME_SECONDS, } from "./jwt.js";
|
package/dist/ip-reputation.d.ts
CHANGED
|
@@ -160,8 +160,12 @@ export interface UrlFeedOptions {
|
|
|
160
160
|
/** Feed name. Defaults to the URL. */
|
|
161
161
|
name?: string;
|
|
162
162
|
/**
|
|
163
|
-
* Custom `fetch` implementation. Defaults to
|
|
164
|
-
*
|
|
163
|
+
* Custom `fetch` implementation. Defaults to an SSRF-hardened
|
|
164
|
+
* {@link fetchGuard} instance so a compromised or malicious feed host cannot
|
|
165
|
+
* redirect the request into internal/link-local space (cloud metadata, etc.).
|
|
166
|
+
* Override with your own client for a non-standard runtime, or with
|
|
167
|
+
* `fetchGuard({ allowPrivate: true })` for an intentionally internal feed
|
|
168
|
+
* mirror.
|
|
165
169
|
*/
|
|
166
170
|
fetchImpl?: typeof fetch;
|
|
167
171
|
/** Extra request headers (e.g. an API token for a commercial feed). */
|
|
@@ -174,6 +178,8 @@ export interface UrlFeedOptions {
|
|
|
174
178
|
* are skipped by {@link ipReputation}, so a partially-malformed feed still loads
|
|
175
179
|
* its good entries.
|
|
176
180
|
*
|
|
181
|
+
* The outbound fetch is SSRF-hardened by default (see {@link UrlFeedOptions.fetchImpl}).
|
|
182
|
+
*
|
|
177
183
|
* @param url - The feed URL.
|
|
178
184
|
* @param opts - Optional feed name, custom `fetch`, and request headers.
|
|
179
185
|
* @returns A feed ready to pass to {@link IpReputationOptions.feeds}.
|
package/dist/ip-reputation.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* @since 0.37.0
|
|
46
46
|
*/
|
|
47
47
|
import { ForbiddenError } from "./errors.js";
|
|
48
|
+
import { fetchGuard } from "./fetch-guard.js";
|
|
48
49
|
import { compileCidrMatcher, matchesMatcher, parseIp, } from "./ip-restriction.js";
|
|
49
50
|
const DEFAULT_REFRESH_MS = 60 * 60_000;
|
|
50
51
|
const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
@@ -74,6 +75,8 @@ function parseFeedLine(line) {
|
|
|
74
75
|
* are skipped by {@link ipReputation}, so a partially-malformed feed still loads
|
|
75
76
|
* its good entries.
|
|
76
77
|
*
|
|
78
|
+
* The outbound fetch is SSRF-hardened by default (see {@link UrlFeedOptions.fetchImpl}).
|
|
79
|
+
*
|
|
77
80
|
* @param url - The feed URL.
|
|
78
81
|
* @param opts - Optional feed name, custom `fetch`, and request headers.
|
|
79
82
|
* @returns A feed ready to pass to {@link IpReputationOptions.feeds}.
|
|
@@ -81,7 +84,10 @@ function parseFeedLine(line) {
|
|
|
81
84
|
*/
|
|
82
85
|
export function urlFeed(url, opts = {}) {
|
|
83
86
|
const name = opts.name ?? url;
|
|
84
|
-
|
|
87
|
+
// Secure-by-default: route the outbound feed fetch through fetchGuard() so
|
|
88
|
+
// redirects are re-validated per hop and internal/metadata targets are
|
|
89
|
+
// refused, matching createWebhookSender's posture. Callers can override.
|
|
90
|
+
const doFetch = opts.fetchImpl ?? fetchGuard();
|
|
85
91
|
return {
|
|
86
92
|
name,
|
|
87
93
|
async fetch(signal) {
|
package/dist/jwk.js
CHANGED
|
@@ -235,7 +235,7 @@ export function jwk(opts) {
|
|
|
235
235
|
});
|
|
236
236
|
return cachedVerifier;
|
|
237
237
|
}
|
|
238
|
-
|
|
238
|
+
const authHooks = {
|
|
239
239
|
async beforeHandle(ctx) {
|
|
240
240
|
const header = ctx.request.headers.get("authorization") ?? "";
|
|
241
241
|
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
@@ -268,6 +268,11 @@ export function jwk(opts) {
|
|
|
268
268
|
return undefined;
|
|
269
269
|
},
|
|
270
270
|
};
|
|
271
|
+
// Same global symbol as middleware's AUTH_HOOK_MARKER (stamped inline to keep
|
|
272
|
+
// the middleware module out of jwk's bundle): lets the route-auth boot guard
|
|
273
|
+
// recognize that a route declaring `auth:` is actually enforced here.
|
|
274
|
+
authHooks[Symbol.for("daloyjs.auth.hook")] = true;
|
|
275
|
+
return authHooks;
|
|
271
276
|
}
|
|
272
277
|
function extractScopes(payload) {
|
|
273
278
|
// RFC 8693 / OAuth2: `scope` is a space-delimited string; some IdPs emit
|
package/dist/mcp.d.ts
CHANGED
|
@@ -39,9 +39,16 @@ export type McpJsonObject = {
|
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
41
|
* JSON Schema fragment advertised to MCP clients for a tool or prompt
|
|
42
|
-
* argument object.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* argument object.
|
|
43
|
+
*
|
|
44
|
+
* For a tool's `inputSchema`, DaloyJS enforces the commonly-used,
|
|
45
|
+
* security-relevant subset of JSON Schema server-side (see
|
|
46
|
+
* {@link validateMcpInput}) BEFORE the tool handler runs, rejecting a
|
|
47
|
+
* `tools/call` whose arguments violate it with JSON-RPC `-32602`. Keywords
|
|
48
|
+
* outside that subset (`pattern`, `format`, `$ref`,
|
|
49
|
+
* `anyOf`/`oneOf`/`allOf`, …) are advertised to clients but NOT enforced —
|
|
50
|
+
* validate any constraint expressed only through those keywords inside your
|
|
51
|
+
* handler before touching databases, files, or remote services.
|
|
45
52
|
*
|
|
46
53
|
* @since 1.0.0
|
|
47
54
|
*/
|
|
@@ -195,8 +202,11 @@ export interface McpToolAnnotations {
|
|
|
195
202
|
* Handler for a single MCP tool.
|
|
196
203
|
*
|
|
197
204
|
* @typeParam TArgs - Type expected in `params.arguments` for this tool.
|
|
198
|
-
* @param args - Tool arguments supplied by the MCP client. They
|
|
199
|
-
*
|
|
205
|
+
* @param args - Tool arguments supplied by the MCP client. They have already
|
|
206
|
+
* been validated against this tool's `inputSchema` (enforced subset — see
|
|
207
|
+
* {@link validateMcpInput}) and had prototype-pollution keys stripped, so the
|
|
208
|
+
* declared shape holds at runtime. Constraints expressed only through
|
|
209
|
+
* unsupported schema keywords (e.g. `pattern`) remain the handler's job.
|
|
200
210
|
* @param ctx - Request metadata and the original HTTP request.
|
|
201
211
|
* @returns Text shorthand or a full {@link McpToolResult}.
|
|
202
212
|
* @throws {McpToolError} for caller-correctable failures that should be
|
|
@@ -209,9 +219,11 @@ export type McpToolHandler<TArgs extends Record<string, unknown> = Record<string
|
|
|
209
219
|
* Definition of a callable MCP tool.
|
|
210
220
|
*
|
|
211
221
|
* Tools are model-controlled in MCP: clients may let the language model decide
|
|
212
|
-
* when to call them. Treat every tool as a public API operation
|
|
213
|
-
*
|
|
214
|
-
*
|
|
222
|
+
* when to call them. Treat every tool as a public API operation. DaloyJS
|
|
223
|
+
* enforces the tool's `inputSchema` (enforced subset — see
|
|
224
|
+
* {@link validateMcpInput}) before the handler runs; you remain responsible for
|
|
225
|
+
* authentication, authorization, rate limits, and any validation beyond that
|
|
226
|
+
* subset before side effects.
|
|
215
227
|
*
|
|
216
228
|
* @typeParam TArgs - Type expected by this tool's handler.
|
|
217
229
|
* @since 1.0.0
|
|
@@ -492,6 +504,31 @@ export interface McpHandlerOptions {
|
|
|
492
504
|
* @since 1.0.0
|
|
493
505
|
*/
|
|
494
506
|
export type McpHandler = (request: Request) => Promise<Response>;
|
|
507
|
+
/**
|
|
508
|
+
* Minimal, dependency-free JSON Schema validator for MCP tool arguments.
|
|
509
|
+
*
|
|
510
|
+
* DaloyJS core bundles no third-party schema library, so this implements the
|
|
511
|
+
* commonly-used, security-relevant subset of JSON Schema — enough to reject the
|
|
512
|
+
* untrusted `tools/call` argument shapes that matter before a tool handler
|
|
513
|
+
* runs: wrong `type` (including `integer`), missing `required` properties,
|
|
514
|
+
* unexpected keys under `additionalProperties: false`, `enum`/`const`
|
|
515
|
+
* violations, and basic string/number/array bounds (`minLength`/`maxLength`,
|
|
516
|
+
* `minimum`/`maximum`, `minItems`/`maxItems`). Nested `properties`, `items`,
|
|
517
|
+
* and object-form `additionalProperties` are validated recursively.
|
|
518
|
+
*
|
|
519
|
+
* Keywords outside this subset (`pattern`, `format`, `$ref`,
|
|
520
|
+
* `anyOf`/`oneOf`/`allOf`, etc.) are intentionally NOT enforced — notably
|
|
521
|
+
* `pattern` is skipped so a developer-authored regex can never become a ReDoS
|
|
522
|
+
* sink against attacker-controlled input. Handlers must still validate any
|
|
523
|
+
* constraint expressed only through those keywords.
|
|
524
|
+
*
|
|
525
|
+
* @param schema - The tool's advertised `inputSchema`.
|
|
526
|
+
* @param value - The untrusted `params.arguments` value from the client.
|
|
527
|
+
* @returns A list of human-readable validation errors; empty when the value
|
|
528
|
+
* satisfies the enforced subset of the schema.
|
|
529
|
+
* @since 1.0.0
|
|
530
|
+
*/
|
|
531
|
+
export declare function validateMcpInput(schema: McpJsonSchema, value: unknown): string[];
|
|
495
532
|
/**
|
|
496
533
|
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
497
534
|
*
|
|
@@ -544,6 +581,25 @@ export type McpHandler = (request: Request) => Promise<Response>;
|
|
|
544
581
|
* @since 1.0.0
|
|
545
582
|
*/
|
|
546
583
|
export declare function createMcpHandler(options: McpHandlerOptions): McpHandler;
|
|
584
|
+
/**
|
|
585
|
+
* Options for {@link mcpRoutes}.
|
|
586
|
+
*
|
|
587
|
+
* @since 1.0.0
|
|
588
|
+
*/
|
|
589
|
+
export interface McpRoutesOptions {
|
|
590
|
+
/**
|
|
591
|
+
* Set `true` to intentionally expose the MCP endpoint WITHOUT authentication,
|
|
592
|
+
* opting the `POST` transport out of the App's production route-auth boot
|
|
593
|
+
* guard. Only do this for a genuinely public MCP server — MCP tools are
|
|
594
|
+
* model-controlled and can trigger side effects, so an unauthenticated
|
|
595
|
+
* endpoint is a high-impact default. When left `false` (the default), a
|
|
596
|
+
* production `secureDefaults` App refuses to boot unless an authentication
|
|
597
|
+
* hook covers the MCP route.
|
|
598
|
+
*
|
|
599
|
+
* @defaultValue false
|
|
600
|
+
*/
|
|
601
|
+
public?: boolean;
|
|
602
|
+
}
|
|
547
603
|
/**
|
|
548
604
|
* Build the Daloy route definitions for a Streamable HTTP MCP endpoint.
|
|
549
605
|
*
|
|
@@ -552,8 +608,16 @@ export declare function createMcpHandler(options: McpHandlerOptions): McpHandler
|
|
|
552
608
|
* its public contract and auth policy, while the MCP server can use its own
|
|
553
609
|
* bearer token, rate limit, network allowlist, and tool set.
|
|
554
610
|
*
|
|
611
|
+
* By default the `POST` transport route is stamped so that a production
|
|
612
|
+
* `secureDefaults` App **refuses to boot** unless an authentication hook covers
|
|
613
|
+
* it — MCP tools are model-controlled and side-effecting. Cover the route with
|
|
614
|
+
* an auth middleware (e.g. `app.use(bearerAuth({ ... }))`), or pass
|
|
615
|
+
* `{ public: true }` to intentionally expose a public MCP server.
|
|
616
|
+
*
|
|
555
617
|
* @param path - Public MCP endpoint path, usually `"/mcp"`.
|
|
556
618
|
* @param handler - Handler returned by {@link createMcpHandler}.
|
|
619
|
+
* @param options - See {@link McpRoutesOptions}; pass `{ public: true }` to opt
|
|
620
|
+
* out of the auth boot guard.
|
|
557
621
|
* @returns Route definitions for `POST`, `GET`, and `OPTIONS` on the same
|
|
558
622
|
* path. `POST` is the actual MCP transport; `GET` gives a human-readable
|
|
559
623
|
* 405 hint because this helper does not open server-initiated SSE streams;
|
|
@@ -564,11 +628,18 @@ export declare function createMcpHandler(options: McpHandlerOptions): McpHandler
|
|
|
564
628
|
* const app = new App();
|
|
565
629
|
* const mcp = createMcpHandler({ serverInfo, tools });
|
|
566
630
|
*
|
|
631
|
+
* // Authenticated MCP server (satisfies the production boot guard):
|
|
632
|
+
* app.use(bearerAuth({ validate: (t) => timingSafeEqual(t, process.env.MCP_TOKEN!) }));
|
|
567
633
|
* for (const route of mcpRoutes("/mcp", mcp)) {
|
|
568
634
|
* app.route(route);
|
|
569
635
|
* }
|
|
636
|
+
*
|
|
637
|
+
* // ...or an intentionally public MCP server:
|
|
638
|
+
* for (const route of mcpRoutes("/mcp", mcp, { public: true })) {
|
|
639
|
+
* app.route(route);
|
|
640
|
+
* }
|
|
570
641
|
* ```
|
|
571
642
|
*
|
|
572
643
|
* @since 1.0.0
|
|
573
644
|
*/
|
|
574
|
-
export declare function mcpRoutes(path: PathString, handler: McpHandler): RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
|
|
645
|
+
export declare function mcpRoutes(path: PathString, handler: McpHandler, options?: McpRoutesOptions): RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeJsonParse } from "./security.js";
|
|
1
2
|
/**
|
|
2
3
|
* Latest MCP protocol version DaloyJS negotiates by default.
|
|
3
4
|
*
|
|
@@ -125,6 +126,165 @@ function asRecord(value) {
|
|
|
125
126
|
? value
|
|
126
127
|
: {};
|
|
127
128
|
}
|
|
129
|
+
/** Hard cap on reported validation errors so a hostile payload can't inflate the response. */
|
|
130
|
+
const MAX_MCP_VALIDATION_ERRORS = 20;
|
|
131
|
+
/** Recursion-depth cap so a deeply-nested payload can't exhaust the stack. */
|
|
132
|
+
const MAX_MCP_SCHEMA_DEPTH = 64;
|
|
133
|
+
/** Narrow an arbitrary JSON value to a schema object (`{}`), excluding arrays/null. */
|
|
134
|
+
function isSchemaObject(v) {
|
|
135
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
136
|
+
}
|
|
137
|
+
/** Report the JSON type of a value using JSON Schema's type names. */
|
|
138
|
+
function jsonTypeOf(v) {
|
|
139
|
+
if (v === null)
|
|
140
|
+
return "null";
|
|
141
|
+
if (Array.isArray(v))
|
|
142
|
+
return "array";
|
|
143
|
+
return typeof v;
|
|
144
|
+
}
|
|
145
|
+
/** Test a value against a single JSON Schema `type` keyword. */
|
|
146
|
+
function matchesJsonType(type, value) {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case "integer":
|
|
149
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
150
|
+
case "number":
|
|
151
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
152
|
+
case "string":
|
|
153
|
+
return typeof value === "string";
|
|
154
|
+
case "boolean":
|
|
155
|
+
return typeof value === "boolean";
|
|
156
|
+
case "null":
|
|
157
|
+
return value === null;
|
|
158
|
+
case "object":
|
|
159
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
160
|
+
case "array":
|
|
161
|
+
return Array.isArray(value);
|
|
162
|
+
default:
|
|
163
|
+
// Unknown type keyword — do not reject; treat as unconstrained.
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Structural equality for `enum`/`const` comparison (sufficient for JSON scalars/objects). */
|
|
168
|
+
function deepEqualJson(a, b) {
|
|
169
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
170
|
+
}
|
|
171
|
+
/** Recursive worker for {@link validateMcpInput}. Pushes human-readable errors into `errors`. */
|
|
172
|
+
function validateSchemaNode(schema, value, path, errors, depth) {
|
|
173
|
+
if (errors.length >= MAX_MCP_VALIDATION_ERRORS)
|
|
174
|
+
return;
|
|
175
|
+
if (depth > MAX_MCP_SCHEMA_DEPTH) {
|
|
176
|
+
errors.push(`${path}: exceeds maximum validation depth`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// type (string or array-of-strings). A type mismatch stops deeper,
|
|
180
|
+
// type-dependent checks for this node to avoid a cascade of noise.
|
|
181
|
+
const typeKw = schema.type;
|
|
182
|
+
if (typeof typeKw === "string") {
|
|
183
|
+
if (!matchesJsonType(typeKw, value)) {
|
|
184
|
+
errors.push(`${path}: expected ${typeKw}, got ${jsonTypeOf(value)}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (Array.isArray(typeKw)) {
|
|
189
|
+
const types = typeKw.filter((t) => typeof t === "string");
|
|
190
|
+
if (types.length > 0 && !types.some((t) => matchesJsonType(t, value))) {
|
|
191
|
+
errors.push(`${path}: expected one of [${types.join(", ")}], got ${jsonTypeOf(value)}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((e) => deepEqualJson(e, value))) {
|
|
196
|
+
errors.push(`${path}: value is not one of the allowed enum values`);
|
|
197
|
+
}
|
|
198
|
+
if ("const" in schema && !deepEqualJson(schema.const, value)) {
|
|
199
|
+
errors.push(`${path}: value does not equal the required constant`);
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === "string") {
|
|
202
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
203
|
+
errors.push(`${path}: string shorter than minLength ${schema.minLength}`);
|
|
204
|
+
}
|
|
205
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
206
|
+
errors.push(`${path}: string longer than maxLength ${schema.maxLength}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === "number") {
|
|
210
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
211
|
+
errors.push(`${path}: number below minimum ${schema.minimum}`);
|
|
212
|
+
}
|
|
213
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) {
|
|
214
|
+
errors.push(`${path}: number above maximum ${schema.maximum}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
219
|
+
errors.push(`${path}: array has fewer than minItems ${schema.minItems}`);
|
|
220
|
+
}
|
|
221
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
222
|
+
errors.push(`${path}: array has more than maxItems ${schema.maxItems}`);
|
|
223
|
+
}
|
|
224
|
+
if (isSchemaObject(schema.items)) {
|
|
225
|
+
for (let i = 0; i < value.length; i++) {
|
|
226
|
+
validateSchemaNode(schema.items, value[i], `${path}[${i}]`, errors, depth + 1);
|
|
227
|
+
if (errors.length >= MAX_MCP_VALIDATION_ERRORS)
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
233
|
+
const obj = value;
|
|
234
|
+
const props = isSchemaObject(schema.properties) ? schema.properties : undefined;
|
|
235
|
+
if (Array.isArray(schema.required)) {
|
|
236
|
+
for (const req of schema.required) {
|
|
237
|
+
if (typeof req === "string" && !Object.prototype.hasOwnProperty.call(obj, req)) {
|
|
238
|
+
errors.push(`${path}.${req}: required property is missing`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const addl = schema.additionalProperties;
|
|
243
|
+
for (const key of Object.keys(obj)) {
|
|
244
|
+
const sub = props && isSchemaObject(props[key]) ? props[key] : undefined;
|
|
245
|
+
if (sub) {
|
|
246
|
+
validateSchemaNode(sub, obj[key], `${path}.${key}`, errors, depth + 1);
|
|
247
|
+
}
|
|
248
|
+
else if (addl === false) {
|
|
249
|
+
errors.push(`${path}.${key}: unexpected property (additionalProperties is false)`);
|
|
250
|
+
}
|
|
251
|
+
else if (isSchemaObject(addl)) {
|
|
252
|
+
validateSchemaNode(addl, obj[key], `${path}.${key}`, errors, depth + 1);
|
|
253
|
+
}
|
|
254
|
+
if (errors.length >= MAX_MCP_VALIDATION_ERRORS)
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Minimal, dependency-free JSON Schema validator for MCP tool arguments.
|
|
261
|
+
*
|
|
262
|
+
* DaloyJS core bundles no third-party schema library, so this implements the
|
|
263
|
+
* commonly-used, security-relevant subset of JSON Schema — enough to reject the
|
|
264
|
+
* untrusted `tools/call` argument shapes that matter before a tool handler
|
|
265
|
+
* runs: wrong `type` (including `integer`), missing `required` properties,
|
|
266
|
+
* unexpected keys under `additionalProperties: false`, `enum`/`const`
|
|
267
|
+
* violations, and basic string/number/array bounds (`minLength`/`maxLength`,
|
|
268
|
+
* `minimum`/`maximum`, `minItems`/`maxItems`). Nested `properties`, `items`,
|
|
269
|
+
* and object-form `additionalProperties` are validated recursively.
|
|
270
|
+
*
|
|
271
|
+
* Keywords outside this subset (`pattern`, `format`, `$ref`,
|
|
272
|
+
* `anyOf`/`oneOf`/`allOf`, etc.) are intentionally NOT enforced — notably
|
|
273
|
+
* `pattern` is skipped so a developer-authored regex can never become a ReDoS
|
|
274
|
+
* sink against attacker-controlled input. Handlers must still validate any
|
|
275
|
+
* constraint expressed only through those keywords.
|
|
276
|
+
*
|
|
277
|
+
* @param schema - The tool's advertised `inputSchema`.
|
|
278
|
+
* @param value - The untrusted `params.arguments` value from the client.
|
|
279
|
+
* @returns A list of human-readable validation errors; empty when the value
|
|
280
|
+
* satisfies the enforced subset of the schema.
|
|
281
|
+
* @since 1.0.0
|
|
282
|
+
*/
|
|
283
|
+
export function validateMcpInput(schema, value) {
|
|
284
|
+
const errors = [];
|
|
285
|
+
validateSchemaNode(schema, value, "arguments", errors, 0);
|
|
286
|
+
return errors;
|
|
287
|
+
}
|
|
128
288
|
function publicTool(tool) {
|
|
129
289
|
const { handler: _handler, ...rest } = tool;
|
|
130
290
|
return rest;
|
|
@@ -382,8 +542,18 @@ export function createMcpHandler(options) {
|
|
|
382
542
|
if (!tool) {
|
|
383
543
|
return rpcError(id, INVALID_PARAMS, `Unknown tool: ${name || "<missing>"}`, undefined, 200, headers);
|
|
384
544
|
}
|
|
545
|
+
// Enforce the tool's advertised inputSchema on the untrusted client
|
|
546
|
+
// arguments BEFORE the handler runs, so a handler is never handed a
|
|
547
|
+
// payload that violates its own contract (wrong types, missing required
|
|
548
|
+
// fields, unexpected keys). Protocol-level validation failures map to
|
|
549
|
+
// JSON-RPC -32602 (Invalid params).
|
|
550
|
+
const rawArgs = params.arguments === undefined ? {} : params.arguments;
|
|
551
|
+
const validationErrors = validateMcpInput(tool.inputSchema, rawArgs);
|
|
552
|
+
if (validationErrors.length > 0) {
|
|
553
|
+
return rpcError(id, INVALID_PARAMS, `Invalid arguments for tool "${name}": ${validationErrors[0]}`, { validationErrors }, 200, headers);
|
|
554
|
+
}
|
|
385
555
|
try {
|
|
386
|
-
const result = await tool.handler(asRecord(
|
|
556
|
+
const result = await tool.handler(asRecord(rawArgs), ctx);
|
|
387
557
|
return rpcResult(id, normalizeToolResult(result), headers);
|
|
388
558
|
}
|
|
389
559
|
catch (error) {
|
|
@@ -523,7 +693,11 @@ export function createMcpHandler(options) {
|
|
|
523
693
|
}
|
|
524
694
|
let message;
|
|
525
695
|
try {
|
|
526
|
-
|
|
696
|
+
// `safeJsonParse` strips `__proto__` / `constructor` / `prototype` keys so
|
|
697
|
+
// an untrusted MCP client cannot smuggle prototype-pollution-shaped keys
|
|
698
|
+
// into a tool handler's arguments — matching the REST body parsers'
|
|
699
|
+
// secure-by-default posture (see `safeJsonParse` in security.ts).
|
|
700
|
+
message = safeJsonParse(raw);
|
|
527
701
|
}
|
|
528
702
|
catch {
|
|
529
703
|
return rpcError(null, PARSE_ERROR, "Invalid JSON in request body.", undefined, 400, headers);
|
|
@@ -565,8 +739,16 @@ export function createMcpHandler(options) {
|
|
|
565
739
|
* its public contract and auth policy, while the MCP server can use its own
|
|
566
740
|
* bearer token, rate limit, network allowlist, and tool set.
|
|
567
741
|
*
|
|
742
|
+
* By default the `POST` transport route is stamped so that a production
|
|
743
|
+
* `secureDefaults` App **refuses to boot** unless an authentication hook covers
|
|
744
|
+
* it — MCP tools are model-controlled and side-effecting. Cover the route with
|
|
745
|
+
* an auth middleware (e.g. `app.use(bearerAuth({ ... }))`), or pass
|
|
746
|
+
* `{ public: true }` to intentionally expose a public MCP server.
|
|
747
|
+
*
|
|
568
748
|
* @param path - Public MCP endpoint path, usually `"/mcp"`.
|
|
569
749
|
* @param handler - Handler returned by {@link createMcpHandler}.
|
|
750
|
+
* @param options - See {@link McpRoutesOptions}; pass `{ public: true }` to opt
|
|
751
|
+
* out of the auth boot guard.
|
|
570
752
|
* @returns Route definitions for `POST`, `GET`, and `OPTIONS` on the same
|
|
571
753
|
* path. `POST` is the actual MCP transport; `GET` gives a human-readable
|
|
572
754
|
* 405 hint because this helper does not open server-initiated SSE streams;
|
|
@@ -577,14 +759,21 @@ export function createMcpHandler(options) {
|
|
|
577
759
|
* const app = new App();
|
|
578
760
|
* const mcp = createMcpHandler({ serverInfo, tools });
|
|
579
761
|
*
|
|
762
|
+
* // Authenticated MCP server (satisfies the production boot guard):
|
|
763
|
+
* app.use(bearerAuth({ validate: (t) => timingSafeEqual(t, process.env.MCP_TOKEN!) }));
|
|
580
764
|
* for (const route of mcpRoutes("/mcp", mcp)) {
|
|
581
765
|
* app.route(route);
|
|
582
766
|
* }
|
|
767
|
+
*
|
|
768
|
+
* // ...or an intentionally public MCP server:
|
|
769
|
+
* for (const route of mcpRoutes("/mcp", mcp, { public: true })) {
|
|
770
|
+
* app.route(route);
|
|
771
|
+
* }
|
|
583
772
|
* ```
|
|
584
773
|
*
|
|
585
774
|
* @since 1.0.0
|
|
586
775
|
*/
|
|
587
|
-
export function mcpRoutes(path, handler) {
|
|
776
|
+
export function mcpRoutes(path, handler, options = {}) {
|
|
588
777
|
const responses = {
|
|
589
778
|
200: { description: "MCP JSON-RPC response", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
590
779
|
202: { description: "MCP notification accepted", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
@@ -594,7 +783,7 @@ export function mcpRoutes(path, handler) {
|
|
|
594
783
|
405: { description: "Unsupported MCP transport method" },
|
|
595
784
|
413: { description: "MCP request body too large" },
|
|
596
785
|
};
|
|
597
|
-
|
|
786
|
+
const routes = [
|
|
598
787
|
{
|
|
599
788
|
method: "POST",
|
|
600
789
|
path,
|
|
@@ -620,4 +809,17 @@ export function mcpRoutes(path, handler) {
|
|
|
620
809
|
handler: ({ request }) => handler(request),
|
|
621
810
|
},
|
|
622
811
|
];
|
|
812
|
+
// Unless explicitly public, stamp the POST transport (the route that executes
|
|
813
|
+
// tools/call) with the global-registry marker the App boot guard reads. GET
|
|
814
|
+
// (405 hint) and OPTIONS (preflight) are not marked: preflight must stay
|
|
815
|
+
// credential-free. Uses the same string as app.ts's MCP_ROUTE_MARKER; kept as
|
|
816
|
+
// a bare Symbol.for so the App core never imports this module.
|
|
817
|
+
if (options.public !== true) {
|
|
818
|
+
for (const route of routes) {
|
|
819
|
+
if (route.method === "POST") {
|
|
820
|
+
route[Symbol.for("daloyjs.mcp.route")] = true;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return routes;
|
|
623
825
|
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -314,6 +314,44 @@ export declare const CORS_WILDCARD_ORIGIN_MARKER: unique symbol;
|
|
|
314
314
|
* @since 0.17.0
|
|
315
315
|
*/
|
|
316
316
|
export declare const CSRF_HOOK_MARKER: unique symbol;
|
|
317
|
+
/**
|
|
318
|
+
* Marker stamped on a {@link Hooks} bundle that authenticates the request —
|
|
319
|
+
* i.e. rejects callers without valid credentials. Built-in auth middlewares
|
|
320
|
+
* (`bearerAuth`, `basicAuth`, `jwk`, `httpSignatureAuth`, `clientCertAuth`)
|
|
321
|
+
* stamp it so the framework's route-auth boot guard can confirm that any route
|
|
322
|
+
* declaring an `auth:` requirement is actually enforced by a hook rather than
|
|
323
|
+
* being silently public (a `security` entry in the OpenAPI doc with no runtime
|
|
324
|
+
* check). Wrap a custom authentication hook with {@link markAuthHook} to opt it
|
|
325
|
+
* into the same guard.
|
|
326
|
+
*
|
|
327
|
+
* @since 1.0.0
|
|
328
|
+
*/
|
|
329
|
+
export declare const AUTH_HOOK_MARKER: unique symbol;
|
|
330
|
+
/**
|
|
331
|
+
* Mark a custom {@link Hooks} bundle as performing request authentication.
|
|
332
|
+
*
|
|
333
|
+
* Use this when you authenticate with your own hook (not one of the built-in
|
|
334
|
+
* auth middlewares) but still declare `auth:` on the protected routes: it
|
|
335
|
+
* stamps {@link AUTH_HOOK_MARKER} so the production route-auth boot guard treats
|
|
336
|
+
* those routes as enforced. It is also the correct escape hatch when
|
|
337
|
+
* authentication is performed by an upstream gateway/mesh and the in-app hook
|
|
338
|
+
* is intentionally a pass-through.
|
|
339
|
+
*
|
|
340
|
+
* @param hooks - The hook bundle to mark (mutated in place and returned).
|
|
341
|
+
* @returns The same `hooks` object, now stamped as an auth hook.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```ts
|
|
345
|
+
* app.use(markAuthHook({
|
|
346
|
+
* async beforeHandle(ctx) {
|
|
347
|
+
* if (!(await myVerify(ctx.request))) throw new UnauthorizedError();
|
|
348
|
+
* },
|
|
349
|
+
* }));
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* @since 1.0.0
|
|
353
|
+
*/
|
|
354
|
+
export declare function markAuthHook(hooks: Hooks): Hooks;
|
|
317
355
|
/** Predicate stamped on a CORS `Hooks` object that returns `true` for allowed origins. */
|
|
318
356
|
export type CorsOriginAllow = (origin: string) => boolean;
|
|
319
357
|
/** Options for {@link cors}. */
|