@daloyjs/core 1.0.0-beta.4 → 1.0.0-beta.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 +50 -46
- package/dist/app.d.ts +42 -21
- package/dist/app.js +100 -116
- package/dist/docs.d.ts +44 -0
- package/dist/docs.js +47 -8
- package/dist/index.d.ts +19 -17
- package/dist/index.js +6 -5
- package/dist/mcp.d.ts +432 -0
- package/dist/mcp.js +419 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/tenancy.d.ts +16 -7
- package/dist/tenancy.js +16 -7
- package/package.json +11 -5
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latest MCP protocol version DaloyJS negotiates by default.
|
|
3
|
+
*
|
|
4
|
+
* @see https://modelcontextprotocol.io/specification/2025-11-25
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
export const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
8
|
+
/**
|
|
9
|
+
* Protocol revisions accepted by {@link createMcpHandler} unless the caller
|
|
10
|
+
* provides an explicit `protocolVersions` list.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
export const MCP_PROTOCOL_VERSIONS = Object.freeze([
|
|
15
|
+
"2024-11-05",
|
|
16
|
+
"2025-03-26",
|
|
17
|
+
"2025-06-18",
|
|
18
|
+
"2025-11-25",
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Default maximum accepted JSON-RPC request body for a DaloyJS MCP endpoint.
|
|
22
|
+
* The cap is intentionally small because MCP calls should carry parameters,
|
|
23
|
+
* not bulk uploads. Raise it per endpoint when a real tool needs larger input.
|
|
24
|
+
*
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
*/
|
|
27
|
+
export const MCP_DEFAULT_MAX_BODY_BYTES = 1 << 18;
|
|
28
|
+
const PARSE_ERROR = -32700;
|
|
29
|
+
const INVALID_REQUEST = -32600;
|
|
30
|
+
const METHOD_NOT_FOUND = -32601;
|
|
31
|
+
const INVALID_PARAMS = -32602;
|
|
32
|
+
const INTERNAL_ERROR = -32603;
|
|
33
|
+
const MCP_JSON_RESPONSE_SCHEMA = {
|
|
34
|
+
"~standard": {
|
|
35
|
+
version: 1,
|
|
36
|
+
vendor: "daloyjs",
|
|
37
|
+
validate: (value) => ({ value }),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Caller-correctable MCP tool/resource/prompt error.
|
|
42
|
+
*
|
|
43
|
+
* Throw this when the model supplied bad arguments, referenced a missing
|
|
44
|
+
* domain object, or otherwise made a recoverable call. Tool errors become
|
|
45
|
+
* `{ isError: true }` tool results; resource and prompt errors become
|
|
46
|
+
* JSON-RPC invalid-params errors. Unexpected errors are treated as internal
|
|
47
|
+
* server failures and are redacted in production.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
*/
|
|
51
|
+
export class McpToolError extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* Create a recoverable MCP handler error.
|
|
54
|
+
*
|
|
55
|
+
* @param message - Safe, caller-visible explanation.
|
|
56
|
+
*/
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "McpToolError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isJsonRpcId(value) {
|
|
63
|
+
return value === null || typeof value === "string" || typeof value === "number";
|
|
64
|
+
}
|
|
65
|
+
function jsonResponse(body, status, extraHeaders) {
|
|
66
|
+
return new Response(JSON.stringify(body), {
|
|
67
|
+
status,
|
|
68
|
+
headers: {
|
|
69
|
+
"content-type": "application/json; charset=utf-8",
|
|
70
|
+
"cache-control": "no-store",
|
|
71
|
+
...(extraHeaders ?? {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function rpcResult(id, result, extraHeaders) {
|
|
76
|
+
return jsonResponse({ jsonrpc: "2.0", id, result }, 200, extraHeaders);
|
|
77
|
+
}
|
|
78
|
+
function rpcError(id, code, message, data, status, extraHeaders) {
|
|
79
|
+
const error = { code, message };
|
|
80
|
+
if (data !== undefined)
|
|
81
|
+
error.data = data;
|
|
82
|
+
return jsonResponse({ jsonrpc: "2.0", id, error }, status, extraHeaders);
|
|
83
|
+
}
|
|
84
|
+
function safeInternalErrorData(error, expose) {
|
|
85
|
+
if (!expose)
|
|
86
|
+
return undefined;
|
|
87
|
+
return { detail: error instanceof Error ? error.message : String(error) };
|
|
88
|
+
}
|
|
89
|
+
function asRecord(value) {
|
|
90
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
91
|
+
? value
|
|
92
|
+
: {};
|
|
93
|
+
}
|
|
94
|
+
function publicTool(tool) {
|
|
95
|
+
const { handler: _handler, ...rest } = tool;
|
|
96
|
+
return rest;
|
|
97
|
+
}
|
|
98
|
+
function publicResource(resource) {
|
|
99
|
+
const { read: _read, ...rest } = resource;
|
|
100
|
+
return rest;
|
|
101
|
+
}
|
|
102
|
+
function publicPrompt(prompt) {
|
|
103
|
+
const { get: _get, ...rest } = prompt;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
function normalizeToolResult(value) {
|
|
107
|
+
return typeof value === "string" ? { content: [{ type: "text", text: value }] } : value;
|
|
108
|
+
}
|
|
109
|
+
function selectedProtocolVersion(requested, supported, preferred) {
|
|
110
|
+
return supported.has(requested) ? requested : preferred;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
114
|
+
*
|
|
115
|
+
* The handler implements the server side of MCP over one HTTP endpoint:
|
|
116
|
+
* `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
|
|
117
|
+
* `resources/read`, `prompts/list`, and `prompts/get`. It accepts JSON-RPC
|
|
118
|
+
* requests over `POST`, acknowledges notifications with `202`, validates the
|
|
119
|
+
* `MCP-Protocol-Version` header, bounds request bodies, and returns JSON-RPC
|
|
120
|
+
* errors for malformed input.
|
|
121
|
+
*
|
|
122
|
+
* It intentionally does not spawn stdio servers, manage OAuth metadata, keep
|
|
123
|
+
* durable sessions, or open server-initiated SSE streams. Use DaloyJS
|
|
124
|
+
* middleware for authentication and authorization, and run this on a dedicated
|
|
125
|
+
* Daloy app when your MCP server has a different trust boundary than your REST
|
|
126
|
+
* API.
|
|
127
|
+
*
|
|
128
|
+
* @param options - Server identity, capabilities, limits, and response headers.
|
|
129
|
+
* @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
|
|
130
|
+
* or for direct use in any web-standard runtime.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const mcp = createMcpHandler({
|
|
135
|
+
* serverInfo: { name: "inventory-mcp", version: "1.0.0" },
|
|
136
|
+
* tools: [
|
|
137
|
+
* {
|
|
138
|
+
* name: "inventory_lookup",
|
|
139
|
+
* description: "Look up inventory by SKU.",
|
|
140
|
+
* inputSchema: {
|
|
141
|
+
* type: "object",
|
|
142
|
+
* properties: { sku: { type: "string" } },
|
|
143
|
+
* required: ["sku"],
|
|
144
|
+
* additionalProperties: false,
|
|
145
|
+
* },
|
|
146
|
+
* handler: async ({ sku }) => `SKU ${sku} has 42 units.`,
|
|
147
|
+
* },
|
|
148
|
+
* ],
|
|
149
|
+
* });
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
*/
|
|
154
|
+
export function createMcpHandler(options) {
|
|
155
|
+
if (options.serverInfo.name.trim().length === 0) {
|
|
156
|
+
throw new TypeError("MCP serverInfo.name is required.");
|
|
157
|
+
}
|
|
158
|
+
if (options.serverInfo.version.trim().length === 0) {
|
|
159
|
+
throw new TypeError("MCP serverInfo.version is required.");
|
|
160
|
+
}
|
|
161
|
+
const protocolVersions = options.protocolVersions ?? MCP_PROTOCOL_VERSIONS;
|
|
162
|
+
if (protocolVersions.length === 0) {
|
|
163
|
+
throw new TypeError("MCP protocolVersions must contain at least one version.");
|
|
164
|
+
}
|
|
165
|
+
const preferred = options.preferredProtocolVersion ?? MCP_PROTOCOL_VERSION;
|
|
166
|
+
const supported = new Set(protocolVersions);
|
|
167
|
+
if (!supported.has(preferred)) {
|
|
168
|
+
throw new TypeError("MCP preferredProtocolVersion must be listed in protocolVersions.");
|
|
169
|
+
}
|
|
170
|
+
const maxBodyBytes = options.maxBodyBytes ?? MCP_DEFAULT_MAX_BODY_BYTES;
|
|
171
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1) {
|
|
172
|
+
throw new TypeError("MCP maxBodyBytes must be a positive safe integer.");
|
|
173
|
+
}
|
|
174
|
+
const tools = options.tools ?? [];
|
|
175
|
+
const resources = options.resources ?? [];
|
|
176
|
+
const prompts = options.prompts ?? [];
|
|
177
|
+
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
|
|
178
|
+
const resourceMap = new Map(resources.map((resource) => [resource.uri, resource]));
|
|
179
|
+
const promptMap = new Map(prompts.map((prompt) => [prompt.name, prompt]));
|
|
180
|
+
if (toolMap.size !== tools.length)
|
|
181
|
+
throw new TypeError("MCP tool names must be unique.");
|
|
182
|
+
if (resourceMap.size !== resources.length)
|
|
183
|
+
throw new TypeError("MCP resource URIs must be unique.");
|
|
184
|
+
if (promptMap.size !== prompts.length)
|
|
185
|
+
throw new TypeError("MCP prompt names must be unique.");
|
|
186
|
+
const exposeInternalErrors = options.exposeInternalErrors ??
|
|
187
|
+
(typeof process === "object" && process.env?.NODE_ENV !== "production");
|
|
188
|
+
const headers = options.headers;
|
|
189
|
+
async function handleRpcRequest(message, request) {
|
|
190
|
+
const id = (message.id ?? null);
|
|
191
|
+
const method = message.method;
|
|
192
|
+
const params = asRecord(message.params);
|
|
193
|
+
const protocolVersion = selectedProtocolVersion(typeof params.protocolVersion === "string"
|
|
194
|
+
? params.protocolVersion
|
|
195
|
+
: (request.headers.get("mcp-protocol-version") ?? ""), supported, preferred);
|
|
196
|
+
const ctx = { request, protocolVersion, id, method };
|
|
197
|
+
switch (method) {
|
|
198
|
+
case "initialize":
|
|
199
|
+
return rpcResult(id, {
|
|
200
|
+
protocolVersion,
|
|
201
|
+
capabilities: {
|
|
202
|
+
...(tools.length > 0 ? { tools: {} } : {}),
|
|
203
|
+
...(resources.length > 0 ? { resources: {} } : {}),
|
|
204
|
+
...(prompts.length > 0 ? { prompts: {} } : {}),
|
|
205
|
+
},
|
|
206
|
+
serverInfo: options.serverInfo,
|
|
207
|
+
...(options.instructions ? { instructions: options.instructions } : {}),
|
|
208
|
+
}, headers);
|
|
209
|
+
case "ping":
|
|
210
|
+
return rpcResult(id, {}, headers);
|
|
211
|
+
case "tools/list":
|
|
212
|
+
return rpcResult(id, { tools: tools.map(publicTool) }, headers);
|
|
213
|
+
case "tools/call": {
|
|
214
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
215
|
+
const tool = toolMap.get(name);
|
|
216
|
+
if (!tool) {
|
|
217
|
+
return rpcError(id, INVALID_PARAMS, `Unknown tool: ${name || "<missing>"}`, undefined, 200, headers);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = await tool.handler(asRecord(params.arguments), ctx);
|
|
221
|
+
return rpcResult(id, normalizeToolResult(result), headers);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof McpToolError) {
|
|
225
|
+
return rpcResult(id, { content: [{ type: "text", text: error.message }], isError: true }, headers);
|
|
226
|
+
}
|
|
227
|
+
return rpcError(id, INTERNAL_ERROR, "Tool execution failed.", safeInternalErrorData(error, exposeInternalErrors), 200, headers);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
case "resources/list":
|
|
231
|
+
return rpcResult(id, { resources: resources.map(publicResource) }, headers);
|
|
232
|
+
case "resources/read": {
|
|
233
|
+
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
234
|
+
const resource = resourceMap.get(uri);
|
|
235
|
+
if (!resource) {
|
|
236
|
+
return rpcError(id, INVALID_PARAMS, `Unknown resource: ${uri || "<missing>"}`, undefined, 200, headers);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const read = await resource.read(ctx);
|
|
240
|
+
return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const message = error instanceof McpToolError ? error.message : "Resource read failed.";
|
|
244
|
+
const data = error instanceof McpToolError
|
|
245
|
+
? undefined
|
|
246
|
+
: safeInternalErrorData(error, exposeInternalErrors);
|
|
247
|
+
return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
case "prompts/list":
|
|
251
|
+
return rpcResult(id, { prompts: prompts.map(publicPrompt) }, headers);
|
|
252
|
+
case "prompts/get": {
|
|
253
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
254
|
+
const prompt = promptMap.get(name);
|
|
255
|
+
if (!prompt) {
|
|
256
|
+
return rpcError(id, INVALID_PARAMS, `Unknown prompt: ${name || "<missing>"}`, undefined, 200, headers);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return rpcResult(id, await prompt.get(asRecord(params.arguments), ctx), headers);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof McpToolError ? error.message : "Prompt rendering failed.";
|
|
263
|
+
const data = error instanceof McpToolError
|
|
264
|
+
? undefined
|
|
265
|
+
: safeInternalErrorData(error, exposeInternalErrors);
|
|
266
|
+
return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
return rpcError(id, METHOD_NOT_FOUND, `Method not found: ${method}`, undefined, 200, headers);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return async function handleMcpRequest(request) {
|
|
274
|
+
if (request.method === "OPTIONS") {
|
|
275
|
+
return new Response(null, {
|
|
276
|
+
status: 204,
|
|
277
|
+
headers: { allow: "GET, POST, OPTIONS", ...(headers ?? {}) },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (request.method === "GET") {
|
|
281
|
+
return jsonResponse({
|
|
282
|
+
transport: "streamable-http",
|
|
283
|
+
protocolVersions,
|
|
284
|
+
capabilities: {
|
|
285
|
+
tools: tools.map((tool) => tool.name),
|
|
286
|
+
resources: resources.map((resource) => resource.uri),
|
|
287
|
+
prompts: prompts.map((prompt) => prompt.name),
|
|
288
|
+
},
|
|
289
|
+
hint: "Send JSON-RPC 2.0 over HTTP POST to this endpoint.",
|
|
290
|
+
}, 405, { allow: "POST, OPTIONS", ...(headers ?? {}) });
|
|
291
|
+
}
|
|
292
|
+
if (request.method !== "POST") {
|
|
293
|
+
return jsonResponse({ error: "MCP Streamable HTTP endpoints accept POST requests." }, 405, {
|
|
294
|
+
allow: "POST, OPTIONS",
|
|
295
|
+
...(headers ?? {}),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
299
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
300
|
+
return rpcError(null, INVALID_REQUEST, "MCP POST requests must use application/json.", undefined, 415, headers);
|
|
301
|
+
}
|
|
302
|
+
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
303
|
+
if (protocolHeader && !supported.has(protocolHeader)) {
|
|
304
|
+
return rpcError(null, INVALID_REQUEST, `Unsupported MCP-Protocol-Version: ${protocolHeader}`, { supported: protocolVersions }, 400, headers);
|
|
305
|
+
}
|
|
306
|
+
const declaredLength = Number(request.headers.get("content-length") ?? "");
|
|
307
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
308
|
+
return rpcError(null, INVALID_REQUEST, "Request body too large.", undefined, 413, headers);
|
|
309
|
+
}
|
|
310
|
+
const body = await request.arrayBuffer();
|
|
311
|
+
if (body.byteLength > maxBodyBytes) {
|
|
312
|
+
return rpcError(null, INVALID_REQUEST, "Request body too large.", undefined, 413, headers);
|
|
313
|
+
}
|
|
314
|
+
let raw;
|
|
315
|
+
try {
|
|
316
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return rpcError(null, PARSE_ERROR, "Request body must be valid UTF-8.", undefined, 400, headers);
|
|
320
|
+
}
|
|
321
|
+
let message;
|
|
322
|
+
try {
|
|
323
|
+
message = JSON.parse(raw);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return rpcError(null, PARSE_ERROR, "Invalid JSON in request body.", undefined, 400, headers);
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(message)) {
|
|
329
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC batch requests are not supported.", undefined, 400, headers);
|
|
330
|
+
}
|
|
331
|
+
if (!message || typeof message !== "object" || message.jsonrpc !== "2.0") {
|
|
332
|
+
return rpcError(null, INVALID_REQUEST, "Request must be a JSON-RPC 2.0 message.", undefined, 400, headers);
|
|
333
|
+
}
|
|
334
|
+
if (message.id !== undefined && !isJsonRpcId(message.id)) {
|
|
335
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC id must be a string, number, or null.", undefined, 400, headers);
|
|
336
|
+
}
|
|
337
|
+
if (message.method === undefined) {
|
|
338
|
+
if (!("result" in message) && !("error" in message)) {
|
|
339
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC message is missing `method`, `result`, or `error`.", undefined, 400, headers);
|
|
340
|
+
}
|
|
341
|
+
return new Response(null, { status: 202, headers });
|
|
342
|
+
}
|
|
343
|
+
if (typeof message.method !== "string") {
|
|
344
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC method must be a string.", undefined, 400, headers);
|
|
345
|
+
}
|
|
346
|
+
if (message.id === undefined) {
|
|
347
|
+
return new Response(null, { status: 202, headers });
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
return await handleRpcRequest(message, request);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return rpcError(message.id, INTERNAL_ERROR, "Internal server error.", safeInternalErrorData(error, exposeInternalErrors), 200, headers);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Build the Daloy route definitions for a Streamable HTTP MCP endpoint.
|
|
359
|
+
*
|
|
360
|
+
* Register each returned route on the Daloy app that should host MCP. A
|
|
361
|
+
* separate app is often the cleanest production shape: the REST API can keep
|
|
362
|
+
* its public contract and auth policy, while the MCP server can use its own
|
|
363
|
+
* bearer token, rate limit, network allowlist, and tool set.
|
|
364
|
+
*
|
|
365
|
+
* @param path - Public MCP endpoint path, usually `"/mcp"`.
|
|
366
|
+
* @param handler - Handler returned by {@link createMcpHandler}.
|
|
367
|
+
* @returns Route definitions for `POST`, `GET`, and `OPTIONS` on the same
|
|
368
|
+
* path. `POST` is the actual MCP transport; `GET` gives a human-readable
|
|
369
|
+
* 405 hint because this helper does not open server-initiated SSE streams;
|
|
370
|
+
* `OPTIONS` supports preflight when CORS middleware is installed.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* const app = new App();
|
|
375
|
+
* const mcp = createMcpHandler({ serverInfo, tools });
|
|
376
|
+
*
|
|
377
|
+
* for (const route of mcpRoutes("/mcp", mcp)) {
|
|
378
|
+
* app.route(route);
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @since 1.0.0
|
|
383
|
+
*/
|
|
384
|
+
export function mcpRoutes(path, handler) {
|
|
385
|
+
const responses = {
|
|
386
|
+
200: { description: "MCP JSON-RPC response", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
387
|
+
202: { description: "MCP notification accepted", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
388
|
+
204: { description: "CORS preflight accepted" },
|
|
389
|
+
400: { description: "Invalid MCP request" },
|
|
390
|
+
405: { description: "Unsupported MCP transport method" },
|
|
391
|
+
413: { description: "MCP request body too large" },
|
|
392
|
+
};
|
|
393
|
+
return [
|
|
394
|
+
{
|
|
395
|
+
method: "POST",
|
|
396
|
+
path,
|
|
397
|
+
operationId: "mcpPost",
|
|
398
|
+
summary: "MCP Streamable HTTP endpoint",
|
|
399
|
+
responses,
|
|
400
|
+
handler: ({ request }) => handler(request),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
method: "GET",
|
|
404
|
+
path,
|
|
405
|
+
operationId: "mcpGet",
|
|
406
|
+
summary: "MCP Streamable HTTP discovery hint",
|
|
407
|
+
responses,
|
|
408
|
+
handler: ({ request }) => handler(request),
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
method: "OPTIONS",
|
|
412
|
+
path,
|
|
413
|
+
operationId: "mcpOptions",
|
|
414
|
+
summary: "MCP Streamable HTTP preflight",
|
|
415
|
+
responses,
|
|
416
|
+
handler: ({ request }) => handler(request),
|
|
417
|
+
},
|
|
418
|
+
];
|
|
419
|
+
}
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:a88e3a69-b278-5771-a67e-40fb774e004e",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-
|
|
7
|
+
"timestamp": "2026-07-01T22:47:12.881Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.6"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.0.0-beta.
|
|
24
|
+
"version": "1.0.0-beta.6",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-1.0.0-beta.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-1.0.0-beta.6",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.0.0-beta.
|
|
51
|
+
"version": "1.0.0-beta.6",
|
|
52
52
|
"tagVersion": 0,
|
|
53
53
|
"patch": false
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"components": [],
|
|
58
58
|
"dependencies": [
|
|
59
59
|
{
|
|
60
|
-
"ref": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-1.0.0-beta.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.
|
|
5
|
+
"name": "@daloyjs/core-1.0.0-beta.6",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.6-a88e3a69-b278-5771-a67e-40fb774e004e",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-
|
|
8
|
+
"created": "2026-07-01T22:47:12.881Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "1.0.0-beta.
|
|
19
|
+
"versionInfo": "1.0.0-beta.6",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.6"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/tenancy.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* once per request, validates and normalizes it, and exposes it on
|
|
6
6
|
* `ctx.state.tenant` for handlers and downstream middleware. It is the
|
|
7
7
|
* single source of truth for "who is this request for" so the per-tenant
|
|
8
|
-
* isolation knobs on the rest of the framework (`rateLimit`
|
|
9
|
-
* `
|
|
10
|
-
* off the same resolved value via {@link tenantScope}.
|
|
8
|
+
* isolation knobs on the rest of the framework (`rateLimit` /
|
|
9
|
+
* `responseCache` `keyGenerator`, `concurrencyLimit` / `idempotency` `scope`)
|
|
10
|
+
* can all key off the same resolved value via {@link tenantScope}.
|
|
11
11
|
*
|
|
12
12
|
* Secure-by-default posture:
|
|
13
13
|
*
|
|
@@ -227,10 +227,19 @@ export interface TenantScopeOptions {
|
|
|
227
227
|
* or poison another tenant's:
|
|
228
228
|
*
|
|
229
229
|
* ```ts
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
230
|
+
* const scope = tenantScope();
|
|
231
|
+
* rateLimit({ windowMs: 60_000, max: 100, keyGenerator: scope });
|
|
232
|
+
* concurrencyLimit({ maxConcurrent: 20, scope });
|
|
233
|
+
* idempotency({ scope }); // CWE-524 cross-tenant cache defense
|
|
234
|
+
* // responseCache's keyGenerator REPLACES the whole key and it takes
|
|
235
|
+
* // ttlSeconds, so fold the tenant in alongside the path yourself:
|
|
236
|
+
* responseCache({
|
|
237
|
+
* ttlSeconds: 30,
|
|
238
|
+
* keyGenerator: (ctx) => {
|
|
239
|
+
* const u = new URL(ctx.request.url);
|
|
240
|
+
* return `${scope(ctx)}:${ctx.request.method} ${u.pathname}${u.search}`;
|
|
241
|
+
* },
|
|
242
|
+
* });
|
|
234
243
|
* ```
|
|
235
244
|
*
|
|
236
245
|
* The `tenant:` prefix keeps these keys from colliding with other key spaces
|
package/dist/tenancy.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* once per request, validates and normalizes it, and exposes it on
|
|
6
6
|
* `ctx.state.tenant` for handlers and downstream middleware. It is the
|
|
7
7
|
* single source of truth for "who is this request for" so the per-tenant
|
|
8
|
-
* isolation knobs on the rest of the framework (`rateLimit`
|
|
9
|
-
* `
|
|
10
|
-
* off the same resolved value via {@link tenantScope}.
|
|
8
|
+
* isolation knobs on the rest of the framework (`rateLimit` /
|
|
9
|
+
* `responseCache` `keyGenerator`, `concurrencyLimit` / `idempotency` `scope`)
|
|
10
|
+
* can all key off the same resolved value via {@link tenantScope}.
|
|
11
11
|
*
|
|
12
12
|
* Secure-by-default posture:
|
|
13
13
|
*
|
|
@@ -270,10 +270,19 @@ export function tenancy(opts) {
|
|
|
270
270
|
* or poison another tenant's:
|
|
271
271
|
*
|
|
272
272
|
* ```ts
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
273
|
+
* const scope = tenantScope();
|
|
274
|
+
* rateLimit({ windowMs: 60_000, max: 100, keyGenerator: scope });
|
|
275
|
+
* concurrencyLimit({ maxConcurrent: 20, scope });
|
|
276
|
+
* idempotency({ scope }); // CWE-524 cross-tenant cache defense
|
|
277
|
+
* // responseCache's keyGenerator REPLACES the whole key and it takes
|
|
278
|
+
* // ttlSeconds, so fold the tenant in alongside the path yourself:
|
|
279
|
+
* responseCache({
|
|
280
|
+
* ttlSeconds: 30,
|
|
281
|
+
* keyGenerator: (ctx) => {
|
|
282
|
+
* const u = new URL(ctx.request.url);
|
|
283
|
+
* return `${scope(ctx)}:${ctx.request.method} ${u.pathname}${u.search}`;
|
|
284
|
+
* },
|
|
285
|
+
* });
|
|
277
286
|
* ```
|
|
278
287
|
*
|
|
279
288
|
* The `tenant:` prefix keeps these keys from colliding with other key spaces
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"framework",
|
|
21
21
|
"http",
|
|
22
22
|
"openapi",
|
|
23
|
+
"mcp",
|
|
23
24
|
"hey-api",
|
|
24
25
|
"contract-first",
|
|
25
26
|
"typescript",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"daloy": "bin/daloy.mjs"
|
|
38
39
|
},
|
|
39
40
|
"engines": {
|
|
40
|
-
"node": "
|
|
41
|
+
"node": "^24.0.0 || >=26.0.0",
|
|
41
42
|
"pnpm": ">=11.0.0"
|
|
42
43
|
},
|
|
43
44
|
"exports": {
|
|
@@ -85,6 +86,10 @@
|
|
|
85
86
|
"types": "./dist/openapi-diff.d.ts",
|
|
86
87
|
"import": "./dist/openapi-diff.js"
|
|
87
88
|
},
|
|
89
|
+
"./mcp": {
|
|
90
|
+
"types": "./dist/mcp.d.ts",
|
|
91
|
+
"import": "./dist/mcp.js"
|
|
92
|
+
},
|
|
88
93
|
"./asyncapi": {
|
|
89
94
|
"types": "./dist/asyncapi.d.ts",
|
|
90
95
|
"import": "./dist/asyncapi.js"
|
|
@@ -227,9 +232,9 @@
|
|
|
227
232
|
}
|
|
228
233
|
},
|
|
229
234
|
"devDependencies": {
|
|
230
|
-
"@hey-api/openapi-ts": "^0.
|
|
231
|
-
"@types/node": "^
|
|
232
|
-
"fast-check": "^
|
|
235
|
+
"@hey-api/openapi-ts": "^0.99.0",
|
|
236
|
+
"@types/node": "^26.0.1",
|
|
237
|
+
"fast-check": "^4.8.0",
|
|
233
238
|
"prettier": "^3.8.3",
|
|
234
239
|
"tsx": "^4.22.3",
|
|
235
240
|
"typescript": "^6.0.3",
|
|
@@ -240,6 +245,7 @@
|
|
|
240
245
|
"dev": "tsc -w -p tsconfig.json",
|
|
241
246
|
"example": "node --import tsx examples/basic.ts",
|
|
242
247
|
"bench": "node --import tsx bench/router.bench.ts",
|
|
248
|
+
"bench:serverless": "node --import tsx bench/serverless-cold-path.bench.ts",
|
|
243
249
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
244
250
|
"test:red-team": "node --import tsx --test tests/red-team-attacks.test.ts tests/red-team-attacks-2.test.ts tests/red-team-attacks-3.test.ts tests/red-team-attacks-4.test.ts tests/red-team-attacks-5.test.ts tests/red-team-attacks-6.test.ts tests/red-team-attacks-7.test.ts tests/red-team-attacks-8.test.ts tests/red-team-attacks-9.test.ts tests/red-team-attacks-10.test.ts",
|
|
245
251
|
"red-team:live": "node --import tsx red-team-live/run.ts",
|