@mastra/elysia 0.0.0
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/CHANGELOG.md +44 -0
- package/dist/__tests__/helpers.d.ts +12 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/auth-middleware.d.ts +7 -0
- package/dist/auth-middleware.d.ts.map +1 -0
- package/dist/helper.d.ts +60 -0
- package/dist/helper.d.ts.map +1 -0
- package/dist/index.cjs +822 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +818 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _mastra_server_auth = require("@mastra/server/auth");
|
|
3
|
+
let _mastra_server_server_adapter = require("@mastra/server/server-adapter");
|
|
4
|
+
let fetch_to_node = require("fetch-to-node");
|
|
5
|
+
let _mastra_core_request_context = require("@mastra/core/request-context");
|
|
6
|
+
//#region src/auth-middleware.ts
|
|
7
|
+
function createAuthMiddleware({ mastra, requiresAuth = true }) {
|
|
8
|
+
return async (ctx) => {
|
|
9
|
+
if (!requiresAuth) return;
|
|
10
|
+
const authConfig = mastra.getServer()?.auth;
|
|
11
|
+
if (!authConfig) return;
|
|
12
|
+
ctx.requestContext ??= new _mastra_core_request_context.RequestContext();
|
|
13
|
+
ctx.mastra ??= mastra;
|
|
14
|
+
const path = new URL(ctx.request.url).pathname;
|
|
15
|
+
const method = ctx.request.method;
|
|
16
|
+
const customRouteAuthConfig = new Map(ctx.customRouteAuthConfig ?? []);
|
|
17
|
+
customRouteAuthConfig.set(`${method}:${path}`, true);
|
|
18
|
+
const authHeader = ctx.request.headers.get("authorization");
|
|
19
|
+
let token = authHeader ? authHeader.replace("Bearer ", "") : null;
|
|
20
|
+
if (!token && ctx.query?.apiKey) token = ctx.query.apiKey;
|
|
21
|
+
const result = await (0, _mastra_server_auth.coreAuthMiddleware)({
|
|
22
|
+
path,
|
|
23
|
+
method,
|
|
24
|
+
getHeader: (name) => ctx.request.headers.get(name) || void 0,
|
|
25
|
+
mastra,
|
|
26
|
+
authConfig,
|
|
27
|
+
customRouteAuthConfig,
|
|
28
|
+
requestContext: ctx.requestContext,
|
|
29
|
+
rawRequest: ctx.request,
|
|
30
|
+
token,
|
|
31
|
+
buildAuthorizeContext: () => ctx
|
|
32
|
+
});
|
|
33
|
+
if (result.action === "error") return new Response(JSON.stringify(result.body), {
|
|
34
|
+
status: result.status,
|
|
35
|
+
headers: { "Content-Type": "application/json" }
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/helper.ts
|
|
41
|
+
/**
|
|
42
|
+
* OpenAPI documentation cache to avoid regenerating on each call.
|
|
43
|
+
* Uses WeakMap to prevent cache collisions between server instances.
|
|
44
|
+
*/
|
|
45
|
+
const openapiDocCache = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* Get OpenAPI documentation from a MastraServer instance, formatted for Elysia's openapi plugin.
|
|
48
|
+
*
|
|
49
|
+
* This function:
|
|
50
|
+
* - Extracts all Mastra routes with their Zod schemas
|
|
51
|
+
* - Converts them to OpenAPI 3.1.0 format
|
|
52
|
+
* - Merges custom routes if registered
|
|
53
|
+
* - Applies the configured route prefix to all paths
|
|
54
|
+
* - Caches the result for performance
|
|
55
|
+
*
|
|
56
|
+
* @param server - The MastraServer instance (must be initialized with init())
|
|
57
|
+
* @param options - Optional configuration for OpenAPI metadata
|
|
58
|
+
* @returns OpenAPI documentation with paths, components, and info
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { getMastraOpenAPIDoc } from '@mastra/elysia';
|
|
63
|
+
*
|
|
64
|
+
* const srv = new MastraServer({ mastra, app, prefix: '/api' });
|
|
65
|
+
* await srv.init();
|
|
66
|
+
*
|
|
67
|
+
* const openAPIDoc = getMastraOpenAPIDoc(srv, {
|
|
68
|
+
* title: 'My Mastra API',
|
|
69
|
+
* version: '1.0.0',
|
|
70
|
+
* description: 'API with weather agent'
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* app.use(openapi({
|
|
74
|
+
* documentation: {
|
|
75
|
+
* info: openAPIDoc.info,
|
|
76
|
+
* paths: openAPIDoc.paths,
|
|
77
|
+
* components: openAPIDoc.components
|
|
78
|
+
* }
|
|
79
|
+
* }));
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
function getMastraOpenAPIDoc(server, options) {
|
|
83
|
+
if (options?.clearCache) openapiDocCache.delete(server);
|
|
84
|
+
const cached = openapiDocCache.get(server);
|
|
85
|
+
if (cached) {
|
|
86
|
+
if (options) return {
|
|
87
|
+
...cached,
|
|
88
|
+
info: {
|
|
89
|
+
...cached.info,
|
|
90
|
+
...options.title && { title: options.title },
|
|
91
|
+
...options.version && { version: options.version },
|
|
92
|
+
...options.description && { description: options.description }
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
const openApiSpec = (0, _mastra_server_server_adapter.generateOpenAPIDocument)(_mastra_server_server_adapter.SERVER_ROUTES, {
|
|
98
|
+
title: options?.title || "Mastra API",
|
|
99
|
+
version: options?.version || "1.0.0",
|
|
100
|
+
description: options?.description || "Mastra Server API"
|
|
101
|
+
});
|
|
102
|
+
const customApiRoutes = server.customApiRoutes;
|
|
103
|
+
if (customApiRoutes && customApiRoutes.length > 0) {
|
|
104
|
+
const customPaths = (0, _mastra_server_server_adapter.convertCustomRoutesToOpenAPIPaths)(customApiRoutes);
|
|
105
|
+
openApiSpec.paths = {
|
|
106
|
+
...openApiSpec.paths,
|
|
107
|
+
...customPaths
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
let paths = openApiSpec.paths || {};
|
|
111
|
+
const prefix = server.prefix;
|
|
112
|
+
if (prefix && prefix !== "/") {
|
|
113
|
+
const prefixedPaths = {};
|
|
114
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
115
|
+
const prefixedPath = `${prefix}${path}`.replace(/\/+/g, "/");
|
|
116
|
+
prefixedPaths[prefixedPath] = pathItem;
|
|
117
|
+
}
|
|
118
|
+
paths = prefixedPaths;
|
|
119
|
+
}
|
|
120
|
+
const result = {
|
|
121
|
+
paths,
|
|
122
|
+
components: openApiSpec.components,
|
|
123
|
+
info: openApiSpec.info
|
|
124
|
+
};
|
|
125
|
+
openapiDocCache.set(server, result);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clear the OpenAPI documentation cache.
|
|
130
|
+
* Call this if routes are added dynamically after initialization.
|
|
131
|
+
*
|
|
132
|
+
* `@param` server - The MastraServer instance to clear cache for.
|
|
133
|
+
* Note: WeakMap does not support clearing all entries. Pass the specific server instance whose cache should be invalidated.
|
|
134
|
+
*/
|
|
135
|
+
function clearMastraOpenAPICache(server) {
|
|
136
|
+
if (server) openapiDocCache.delete(server);
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/index.ts
|
|
140
|
+
/**
|
|
141
|
+
* Normalizes route path parameters to position-based names (:p0, :p1, ...)
|
|
142
|
+
* to avoid Elysia router conflicts when different routes use different
|
|
143
|
+
* parameter names at the same path segment position (e.g. :agentId vs
|
|
144
|
+
* :storedAgentId under /stored/agents/...).
|
|
145
|
+
* Returns the normalized path and the original parameter names in order.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeRouteParams(path) {
|
|
148
|
+
const paramNames = [];
|
|
149
|
+
return {
|
|
150
|
+
normalizedPath: path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
|
|
151
|
+
paramNames.push(paramName);
|
|
152
|
+
return `:p${paramNames.length - 1}`;
|
|
153
|
+
}),
|
|
154
|
+
paramNames
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remaps Elysia's parsed params from position-based names (p0, p1, ...)
|
|
159
|
+
* back to the original parameter names expected by the route handler.
|
|
160
|
+
*/
|
|
161
|
+
function remapParams(rawParams, paramNames) {
|
|
162
|
+
if (paramNames.length === 0) return rawParams;
|
|
163
|
+
const result = {};
|
|
164
|
+
paramNames.forEach((paramName, i) => {
|
|
165
|
+
const key = `p${i}`;
|
|
166
|
+
if (key in rawParams) result[paramName] = rawParams[key];
|
|
167
|
+
});
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
async function createForwardRequest(ctx) {
|
|
171
|
+
const request = ctx.request;
|
|
172
|
+
const method = request.method;
|
|
173
|
+
if (method === "GET" || method === "HEAD") return request;
|
|
174
|
+
const headers = new Headers(request.headers);
|
|
175
|
+
let body;
|
|
176
|
+
if (ctx.body !== void 0) if (typeof ctx.body === "string" || ctx.body instanceof FormData || ctx.body instanceof Blob) body = ctx.body;
|
|
177
|
+
else {
|
|
178
|
+
body = JSON.stringify(ctx.body);
|
|
179
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
180
|
+
}
|
|
181
|
+
else if (!request.bodyUsed) {
|
|
182
|
+
const bodyBuffer = await request.clone().arrayBuffer();
|
|
183
|
+
if (bodyBuffer.byteLength > 0) body = bodyBuffer;
|
|
184
|
+
}
|
|
185
|
+
headers.delete("content-length");
|
|
186
|
+
return new Request(request.url, {
|
|
187
|
+
method,
|
|
188
|
+
headers,
|
|
189
|
+
body,
|
|
190
|
+
signal: request.signal,
|
|
191
|
+
...body ? { duplex: "half" } : {}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function createSafeReadableStream(body) {
|
|
195
|
+
if (!body) return null;
|
|
196
|
+
return new ReadableStream({ async start(controller) {
|
|
197
|
+
const reader = body.getReader();
|
|
198
|
+
try {
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
controller.enqueue(value);
|
|
203
|
+
}
|
|
204
|
+
} catch {} finally {
|
|
205
|
+
controller.close();
|
|
206
|
+
reader.releaseLock();
|
|
207
|
+
}
|
|
208
|
+
} });
|
|
209
|
+
}
|
|
210
|
+
let _hasPermissionPromise;
|
|
211
|
+
function loadHasPermission() {
|
|
212
|
+
if (!_hasPermissionPromise) _hasPermissionPromise = import("@mastra/core/auth/ee").then((m) => m.hasPermission).catch(() => {
|
|
213
|
+
console.error("[@mastra/elysia] Auth features require @mastra/core >= 1.6.0. Please upgrade: npm install @mastra/core@latest");
|
|
214
|
+
});
|
|
215
|
+
return _hasPermissionPromise;
|
|
216
|
+
}
|
|
217
|
+
var MastraServer = class extends _mastra_server_server_adapter.MastraServer {
|
|
218
|
+
createContextMiddleware() {
|
|
219
|
+
return async (ctx) => {
|
|
220
|
+
let bodyRequestContext;
|
|
221
|
+
let paramsRequestContext;
|
|
222
|
+
if (ctx.request.method === "POST" || ctx.request.method === "PUT" || ctx.request.method === "PATCH") {
|
|
223
|
+
if (ctx.request.headers.get("content-type")?.includes("application/json")) try {
|
|
224
|
+
const body = ctx.body ?? await ctx.request.clone().json();
|
|
225
|
+
if (body.requestContext) bodyRequestContext = body.requestContext;
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
if (ctx.request.method === "GET") try {
|
|
229
|
+
const encodedRequestContext = ctx.query?.requestContext;
|
|
230
|
+
if (encodedRequestContext) try {
|
|
231
|
+
paramsRequestContext = JSON.parse(encodedRequestContext);
|
|
232
|
+
} catch {
|
|
233
|
+
try {
|
|
234
|
+
const json = Buffer.from(encodedRequestContext, "base64").toString("utf-8");
|
|
235
|
+
paramsRequestContext = JSON.parse(json);
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
const requestContext = this.mergeRequestContext({
|
|
240
|
+
paramsRequestContext,
|
|
241
|
+
bodyRequestContext
|
|
242
|
+
});
|
|
243
|
+
this.applyRequestMetadataToContext({
|
|
244
|
+
requestContext,
|
|
245
|
+
getHeader: (name) => ctx.request.headers.get(name) || void 0
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
requestContext,
|
|
249
|
+
mastra: this.mastra,
|
|
250
|
+
registeredTools: this.tools || {},
|
|
251
|
+
taskStore: this.taskStore,
|
|
252
|
+
abortSignal: ctx.request.signal,
|
|
253
|
+
customRouteAuthConfig: this.customRouteAuthConfig
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async stream(route, _res, result) {
|
|
258
|
+
const streamFormat = route.streamFormat || "stream";
|
|
259
|
+
const encoder = new TextEncoder();
|
|
260
|
+
const headers = streamFormat === "sse" ? {
|
|
261
|
+
"Content-Type": "text/event-stream",
|
|
262
|
+
"Cache-Control": "no-cache",
|
|
263
|
+
Connection: "keep-alive",
|
|
264
|
+
"X-Accel-Buffering": "no"
|
|
265
|
+
} : { "Content-Type": "text/plain" };
|
|
266
|
+
const stream = new ReadableStream({ start: async (controller) => {
|
|
267
|
+
const reader = (result instanceof ReadableStream ? result : result.fullStream).getReader();
|
|
268
|
+
try {
|
|
269
|
+
if (streamFormat === "sse" && route.sseFlushOnConnect) controller.enqueue(encoder.encode(": connected\n\n"));
|
|
270
|
+
while (true) {
|
|
271
|
+
const { done, value } = await reader.read();
|
|
272
|
+
if (done) break;
|
|
273
|
+
if (value) {
|
|
274
|
+
if (streamFormat === "sse" && typeof value === "string" && value.startsWith(":")) {
|
|
275
|
+
controller.enqueue(encoder.encode(value));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const outputValue = this.streamOptions?.redact ?? true ? (0, _mastra_server_server_adapter.redactStreamChunk)(value) : value;
|
|
279
|
+
const serialized = (0, _mastra_server_server_adapter.serializeStreamChunk)(outputValue);
|
|
280
|
+
if (!serialized.ok) {
|
|
281
|
+
this.mastra.getLogger()?.error("Failed to serialize stream chunk, skipping", {
|
|
282
|
+
path: route.path,
|
|
283
|
+
chunkType: outputValue?.type,
|
|
284
|
+
error: serialized.error.message
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (streamFormat === "sse") controller.enqueue(encoder.encode(`data: ${serialized.json}\n\n`));
|
|
289
|
+
else controller.enqueue(encoder.encode(serialized.json + ""));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (streamFormat === "sse") controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
293
|
+
controller.close();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.mastra.getLogger()?.error("Error in stream processing", { error: error instanceof Error ? {
|
|
296
|
+
message: error.message,
|
|
297
|
+
stack: error.stack
|
|
298
|
+
} : error });
|
|
299
|
+
controller.error(error);
|
|
300
|
+
} finally {
|
|
301
|
+
await reader.cancel();
|
|
302
|
+
}
|
|
303
|
+
} });
|
|
304
|
+
return new Response(stream, { headers });
|
|
305
|
+
}
|
|
306
|
+
async getParams(route, request) {
|
|
307
|
+
const url = new URL(request.url);
|
|
308
|
+
const { paramNames } = normalizeRouteParams(route.path);
|
|
309
|
+
const rawUrlParams = request.params || request.ctx?.params || {};
|
|
310
|
+
const urlParams = paramNames.length > 0 ? remapParams(rawUrlParams, paramNames) : rawUrlParams;
|
|
311
|
+
const queryParams = (0, _mastra_server_server_adapter.normalizeQueryParams)(Object.fromEntries(url.searchParams));
|
|
312
|
+
let body;
|
|
313
|
+
let bodyParseError;
|
|
314
|
+
if (route.method === "POST" || route.method === "PUT" || route.method === "PATCH" || route.method === "DELETE") {
|
|
315
|
+
const contentType = request.headers.get("content-type") || "";
|
|
316
|
+
if (contentType.includes("multipart/form-data")) try {
|
|
317
|
+
const formData = await request.formData();
|
|
318
|
+
body = await this.parseFormData(formData);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this.mastra.getLogger()?.error("Failed to parse multipart form data", { error: error instanceof Error ? {
|
|
321
|
+
message: error.message,
|
|
322
|
+
stack: error.stack
|
|
323
|
+
} : error });
|
|
324
|
+
if (error instanceof Error && error.message.toLowerCase().includes("size")) throw error;
|
|
325
|
+
bodyParseError = { message: error instanceof Error ? error.message : "Failed to parse multipart form data" };
|
|
326
|
+
}
|
|
327
|
+
else if (contentType.includes("application/json")) {
|
|
328
|
+
const bodyText = await request.clone().text();
|
|
329
|
+
if (bodyText && bodyText.trim().length > 0) try {
|
|
330
|
+
body = JSON.parse(bodyText);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.mastra.getLogger()?.error("Failed to parse JSON body", { error: error instanceof Error ? {
|
|
333
|
+
message: error.message,
|
|
334
|
+
stack: error.stack
|
|
335
|
+
} : error });
|
|
336
|
+
bodyParseError = { message: error instanceof Error ? error.message : "Invalid JSON in request body" };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
urlParams,
|
|
342
|
+
queryParams,
|
|
343
|
+
body,
|
|
344
|
+
bodyParseError
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Parse FormData into a plain object, converting File objects to Buffers.
|
|
349
|
+
* Handles both native FormData and Elysia's pre-parsed object format.
|
|
350
|
+
*/
|
|
351
|
+
async parseFormData(data) {
|
|
352
|
+
const result = {};
|
|
353
|
+
if (data instanceof FormData) {
|
|
354
|
+
for (const [key, value] of data.entries()) if (value instanceof File) {
|
|
355
|
+
const arrayBuffer = await value.arrayBuffer();
|
|
356
|
+
result[key] = Buffer.from(arrayBuffer);
|
|
357
|
+
} else if (typeof value === "string") try {
|
|
358
|
+
result[key] = JSON.parse(value);
|
|
359
|
+
} catch {
|
|
360
|
+
result[key] = value;
|
|
361
|
+
}
|
|
362
|
+
else result[key] = value;
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
for (const [key, value] of Object.entries(data)) if (value && typeof value === "object" && "arrayBuffer" in value && typeof value.arrayBuffer === "function") {
|
|
366
|
+
const arrayBuffer = await value.arrayBuffer();
|
|
367
|
+
result[key] = Buffer.from(arrayBuffer);
|
|
368
|
+
} else if (typeof value === "string") try {
|
|
369
|
+
result[key] = JSON.parse(value);
|
|
370
|
+
} catch {
|
|
371
|
+
result[key] = value;
|
|
372
|
+
}
|
|
373
|
+
else result[key] = value;
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
async sendResponse(route, response, result, prefix) {
|
|
377
|
+
const resolvedPrefix = prefix ?? this.prefix ?? "";
|
|
378
|
+
const refreshHeaders = {};
|
|
379
|
+
if (result && typeof result === "object" && "__refreshHeaders" in result) {
|
|
380
|
+
const headers = result.__refreshHeaders;
|
|
381
|
+
for (const [key, value] of Object.entries(headers)) refreshHeaders[key] = value;
|
|
382
|
+
delete result.__refreshHeaders;
|
|
383
|
+
}
|
|
384
|
+
if (route.responseType === "json") return new Response(JSON.stringify(result), {
|
|
385
|
+
status: 200,
|
|
386
|
+
headers: {
|
|
387
|
+
"Content-Type": "application/json",
|
|
388
|
+
...refreshHeaders
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
else if (route.responseType === "stream") return this.stream(route, response, result);
|
|
392
|
+
else if (route.responseType === "datastream-response") {
|
|
393
|
+
const fetchResponse = result;
|
|
394
|
+
const headers = new Headers(fetchResponse.headers);
|
|
395
|
+
headers.delete("Transfer-Encoding");
|
|
396
|
+
return new Response(createSafeReadableStream(fetchResponse.body), {
|
|
397
|
+
status: fetchResponse.status,
|
|
398
|
+
statusText: fetchResponse.statusText,
|
|
399
|
+
headers
|
|
400
|
+
});
|
|
401
|
+
} else if (route.responseType === "mcp-http") {
|
|
402
|
+
const { server, httpPath, mcpOptions: routeMcpOptions } = result;
|
|
403
|
+
const forwardRequest = await createForwardRequest(response);
|
|
404
|
+
const { req, res } = (0, fetch_to_node.toReqRes)(forwardRequest);
|
|
405
|
+
const options = {
|
|
406
|
+
...this.mcpOptions,
|
|
407
|
+
...routeMcpOptions
|
|
408
|
+
};
|
|
409
|
+
server.startHTTP({
|
|
410
|
+
url: new URL(forwardRequest.url),
|
|
411
|
+
httpPath: `${resolvedPrefix}${httpPath}`,
|
|
412
|
+
req,
|
|
413
|
+
res,
|
|
414
|
+
options: Object.keys(options).length > 0 ? options : void 0
|
|
415
|
+
}).catch((e) => {
|
|
416
|
+
this.mastra.getLogger()?.error("[MCP HTTP] Error in background startHTTP:", { error: e instanceof Error ? {
|
|
417
|
+
message: e.message,
|
|
418
|
+
stack: e.stack
|
|
419
|
+
} : e });
|
|
420
|
+
});
|
|
421
|
+
const mcpResponse = await (0, fetch_to_node.toFetchResponse)(res);
|
|
422
|
+
const headers = new Headers(mcpResponse.headers);
|
|
423
|
+
headers.delete("Transfer-Encoding");
|
|
424
|
+
return new Response(createSafeReadableStream(mcpResponse.body), {
|
|
425
|
+
status: mcpResponse.status,
|
|
426
|
+
statusText: mcpResponse.statusText,
|
|
427
|
+
headers
|
|
428
|
+
});
|
|
429
|
+
} else if (route.responseType === "mcp-sse") {
|
|
430
|
+
const { server, ssePath, messagePath } = result;
|
|
431
|
+
const forwardRequest = await createForwardRequest(response);
|
|
432
|
+
const { req, res } = (0, fetch_to_node.toReqRes)(forwardRequest);
|
|
433
|
+
server.startSSE({
|
|
434
|
+
url: new URL(forwardRequest.url),
|
|
435
|
+
ssePath: `${resolvedPrefix}${ssePath}`,
|
|
436
|
+
messagePath: `${resolvedPrefix}${messagePath}`,
|
|
437
|
+
req,
|
|
438
|
+
res
|
|
439
|
+
}).catch((e) => {
|
|
440
|
+
this.mastra.getLogger()?.error("[MCP SSE] Error in background startSSE:", { error: e instanceof Error ? {
|
|
441
|
+
message: e.message,
|
|
442
|
+
stack: e.stack
|
|
443
|
+
} : e });
|
|
444
|
+
});
|
|
445
|
+
const sseResponse = await (0, fetch_to_node.toFetchResponse)(res);
|
|
446
|
+
const headers = new Headers(sseResponse.headers);
|
|
447
|
+
headers.delete("Transfer-Encoding");
|
|
448
|
+
return new Response(createSafeReadableStream(sseResponse.body), {
|
|
449
|
+
status: sseResponse.status,
|
|
450
|
+
statusText: sseResponse.statusText,
|
|
451
|
+
headers
|
|
452
|
+
});
|
|
453
|
+
} else return new Response(null, { status: 500 });
|
|
454
|
+
}
|
|
455
|
+
async registerRoute(app, route, { prefix: prefixParam } = {}) {
|
|
456
|
+
const prefix = prefixParam ?? this.prefix ?? "";
|
|
457
|
+
const { normalizedPath, paramNames } = normalizeRouteParams(`${prefix}${route.path}`);
|
|
458
|
+
const handler = async (ctx) => {
|
|
459
|
+
const authError = await this.checkRouteAuth(route, {
|
|
460
|
+
path: ctx.request.url ? new URL(ctx.request.url).pathname : ctx.path,
|
|
461
|
+
method: ctx.request.method,
|
|
462
|
+
getHeader: (name) => ctx.request.headers.get(name) || void 0,
|
|
463
|
+
getQuery: (name) => ctx.query?.[name],
|
|
464
|
+
requestContext: ctx.requestContext,
|
|
465
|
+
request: ctx.request,
|
|
466
|
+
buildAuthorizeContext: () => ctx
|
|
467
|
+
});
|
|
468
|
+
if (authError) return new Response(JSON.stringify({ error: authError.error }), {
|
|
469
|
+
status: authError.status,
|
|
470
|
+
headers: {
|
|
471
|
+
"Content-Type": "application/json",
|
|
472
|
+
...authError.headers
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
if (this.mastra.getServer()?.auth) {
|
|
476
|
+
const hasPermission = await loadHasPermission();
|
|
477
|
+
if (hasPermission) {
|
|
478
|
+
const userPermissions = ctx.requestContext.get("userPermissions");
|
|
479
|
+
const permissionError = this.checkRoutePermission(route, userPermissions, hasPermission);
|
|
480
|
+
if (permissionError) return new Response(JSON.stringify({
|
|
481
|
+
error: permissionError.error,
|
|
482
|
+
message: permissionError.message
|
|
483
|
+
}), {
|
|
484
|
+
status: permissionError.status,
|
|
485
|
+
headers: { "Content-Type": "application/json" }
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const rawParams = ctx.params || {};
|
|
490
|
+
const urlParams = paramNames.length > 0 ? remapParams(rawParams, paramNames) : rawParams;
|
|
491
|
+
const queryParams = (0, _mastra_server_server_adapter.normalizeQueryParams)(ctx.query || {});
|
|
492
|
+
let body;
|
|
493
|
+
let bodyParseError;
|
|
494
|
+
if (route.method === "POST" || route.method === "PUT" || route.method === "PATCH" || route.method === "DELETE") {
|
|
495
|
+
const maxSize = route.maxBodySize ?? this.bodyLimitOptions?.maxSize;
|
|
496
|
+
const contentLength = ctx.request.headers.get("content-length");
|
|
497
|
+
if (this.bodyLimitOptions && maxSize && contentLength && parseInt(contentLength, 10) > maxSize) {
|
|
498
|
+
let errorResponse = { error: "Request body too large" };
|
|
499
|
+
try {
|
|
500
|
+
errorResponse = this.bodyLimitOptions.onError({ error: "Request body too large" });
|
|
501
|
+
} catch {}
|
|
502
|
+
return new Response(JSON.stringify(errorResponse), {
|
|
503
|
+
status: 413,
|
|
504
|
+
headers: { "Content-Type": "application/json" }
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const contentType = ctx.request.headers.get("content-type") || "";
|
|
508
|
+
if (contentType.includes("multipart/form-data")) try {
|
|
509
|
+
body = ctx.body ? await this.parseFormData(ctx.body) : void 0;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
this.mastra.getLogger()?.error("Failed to parse multipart form data", { error: error instanceof Error ? {
|
|
512
|
+
message: error.message,
|
|
513
|
+
stack: error.stack
|
|
514
|
+
} : error });
|
|
515
|
+
if (error instanceof Error && error.message.toLowerCase().includes("size")) throw error;
|
|
516
|
+
bodyParseError = { message: error instanceof Error ? error.message : "Failed to parse multipart form data" };
|
|
517
|
+
}
|
|
518
|
+
else if (contentType.includes("application/json")) {
|
|
519
|
+
body = ctx.body;
|
|
520
|
+
if (body === void 0 && contentType.includes("application/json")) {
|
|
521
|
+
const bodyText = await ctx.request.clone().text();
|
|
522
|
+
if (bodyText && bodyText.trim().length > 0) try {
|
|
523
|
+
body = JSON.parse(bodyText);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
this.mastra.getLogger()?.error("Failed to parse JSON body", { error: error instanceof Error ? {
|
|
526
|
+
message: error.message,
|
|
527
|
+
stack: error.stack
|
|
528
|
+
} : error });
|
|
529
|
+
bodyParseError = { message: error instanceof Error ? error.message : "Invalid JSON in request body" };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const params = {
|
|
535
|
+
urlParams,
|
|
536
|
+
queryParams,
|
|
537
|
+
body,
|
|
538
|
+
bodyParseError
|
|
539
|
+
};
|
|
540
|
+
if (params.bodyParseError) return new Response(JSON.stringify({
|
|
541
|
+
error: "Invalid request body",
|
|
542
|
+
issues: [{
|
|
543
|
+
field: "body",
|
|
544
|
+
message: params.bodyParseError.message
|
|
545
|
+
}]
|
|
546
|
+
}), {
|
|
547
|
+
status: 400,
|
|
548
|
+
headers: { "Content-Type": "application/json" }
|
|
549
|
+
});
|
|
550
|
+
if (params.queryParams) try {
|
|
551
|
+
params.queryParams = await this.parseQueryParams(route, params.queryParams);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
this.mastra.getLogger()?.error("Error parsing query params", { error: error instanceof Error ? {
|
|
554
|
+
message: error.message,
|
|
555
|
+
stack: error.stack
|
|
556
|
+
} : error });
|
|
557
|
+
if ((0, _mastra_server_server_adapter.isZodError)(error)) {
|
|
558
|
+
const { status, body } = this.resolveValidationError(route, error, "query");
|
|
559
|
+
return new Response(JSON.stringify(body), {
|
|
560
|
+
status,
|
|
561
|
+
headers: { "Content-Type": "application/json" }
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return new Response(JSON.stringify({
|
|
565
|
+
error: "Invalid query parameters",
|
|
566
|
+
issues: [{
|
|
567
|
+
field: "unknown",
|
|
568
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
569
|
+
}]
|
|
570
|
+
}), {
|
|
571
|
+
status: 400,
|
|
572
|
+
headers: { "Content-Type": "application/json" }
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (params.body) try {
|
|
576
|
+
params.body = await this.parseBody(route, params.body);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.mastra.getLogger()?.error("Error parsing body", { error: error instanceof Error ? {
|
|
579
|
+
message: error.message,
|
|
580
|
+
stack: error.stack
|
|
581
|
+
} : error });
|
|
582
|
+
if ((0, _mastra_server_server_adapter.isZodError)(error)) {
|
|
583
|
+
const { status, body } = this.resolveValidationError(route, error, "body");
|
|
584
|
+
return new Response(JSON.stringify(body), {
|
|
585
|
+
status,
|
|
586
|
+
headers: { "Content-Type": "application/json" }
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return new Response(JSON.stringify({
|
|
590
|
+
error: "Invalid request body",
|
|
591
|
+
issues: [{
|
|
592
|
+
field: "unknown",
|
|
593
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
594
|
+
}]
|
|
595
|
+
}), {
|
|
596
|
+
status: 400,
|
|
597
|
+
headers: { "Content-Type": "application/json" }
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (params.urlParams) try {
|
|
601
|
+
params.urlParams = await this.parsePathParams(route, params.urlParams);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
this.mastra.getLogger()?.error("Error parsing path params", { error: error instanceof Error ? {
|
|
604
|
+
message: error.message,
|
|
605
|
+
stack: error.stack
|
|
606
|
+
} : error });
|
|
607
|
+
if ((0, _mastra_server_server_adapter.isZodError)(error)) {
|
|
608
|
+
const { status, body } = this.resolveValidationError(route, error, "path");
|
|
609
|
+
return new Response(JSON.stringify(body), {
|
|
610
|
+
status,
|
|
611
|
+
headers: { "Content-Type": "application/json" }
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return new Response(JSON.stringify({
|
|
615
|
+
error: "Invalid path parameters",
|
|
616
|
+
issues: [{
|
|
617
|
+
field: "unknown",
|
|
618
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
619
|
+
}]
|
|
620
|
+
}), {
|
|
621
|
+
status: 400,
|
|
622
|
+
headers: { "Content-Type": "application/json" }
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const fgaError = await (0, _mastra_server_server_adapter.checkRouteFGA)(this.mastra, route, ctx.requestContext, {
|
|
626
|
+
...params.urlParams,
|
|
627
|
+
...params.queryParams,
|
|
628
|
+
...typeof params.body === "object" ? params.body : {}
|
|
629
|
+
});
|
|
630
|
+
if (fgaError) return new Response(JSON.stringify({
|
|
631
|
+
error: fgaError.error,
|
|
632
|
+
message: fgaError.message
|
|
633
|
+
}), {
|
|
634
|
+
status: fgaError.status,
|
|
635
|
+
headers: { "Content-Type": "application/json" }
|
|
636
|
+
});
|
|
637
|
+
const handlerParams = {
|
|
638
|
+
...params.urlParams,
|
|
639
|
+
...params.queryParams,
|
|
640
|
+
...typeof params.body === "object" ? params.body : {},
|
|
641
|
+
requestContext: ctx.requestContext,
|
|
642
|
+
mastra: ctx.mastra,
|
|
643
|
+
registeredTools: ctx.registeredTools,
|
|
644
|
+
taskStore: ctx.taskStore,
|
|
645
|
+
abortSignal: ctx.abortSignal,
|
|
646
|
+
routePrefix: prefix,
|
|
647
|
+
request: ctx.request
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
const result = await route.handler(handlerParams);
|
|
651
|
+
return this.sendResponse(route, ctx, result, prefix);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
this.mastra.getLogger()?.error("Error calling handler", {
|
|
654
|
+
error: error instanceof Error ? {
|
|
655
|
+
message: error.message,
|
|
656
|
+
stack: error.stack
|
|
657
|
+
} : error,
|
|
658
|
+
path: route.path,
|
|
659
|
+
method: route.method
|
|
660
|
+
});
|
|
661
|
+
if (error && typeof error === "object") {
|
|
662
|
+
if ("status" in error) {
|
|
663
|
+
const status = error.status;
|
|
664
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }), {
|
|
665
|
+
status,
|
|
666
|
+
headers: { "Content-Type": "application/json" }
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if ("details" in error && error.details && typeof error.details === "object" && "status" in error.details) {
|
|
670
|
+
const status = error.details.status;
|
|
671
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }), {
|
|
672
|
+
status,
|
|
673
|
+
headers: { "Content-Type": "application/json" }
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }), {
|
|
678
|
+
status: 500,
|
|
679
|
+
headers: { "Content-Type": "application/json" }
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
app[route.method.toLowerCase()](normalizedPath, handler);
|
|
684
|
+
}
|
|
685
|
+
async registerCustomApiRoutes() {
|
|
686
|
+
const routes = await this.registerSchemaApiRoutes();
|
|
687
|
+
if (!await this.buildCustomRouteHandler(routes)) return;
|
|
688
|
+
for (const route of routes) {
|
|
689
|
+
const method = route.method.toLowerCase();
|
|
690
|
+
const { normalizedPath: customNormalizedPath } = normalizeRouteParams(route.path);
|
|
691
|
+
this.app[method](customNormalizedPath, async (ctx) => {
|
|
692
|
+
const path = new URL(ctx.request.url).pathname;
|
|
693
|
+
const requestMethod = ctx.request.method;
|
|
694
|
+
const matchedRoute = (0, _mastra_server_auth.findMatchingCustomRoute)(path, requestMethod, this.customApiRoutes ?? this.mastra.getServer()?.apiRoutes);
|
|
695
|
+
const shouldRunCustomRouteAuth = (0, _mastra_server_auth.isProtectedCustomRoute)(path, requestMethod, this.customRouteAuthConfig);
|
|
696
|
+
const shouldRunCustomRouteFGA = !!matchedRoute?.route.fga;
|
|
697
|
+
if (shouldRunCustomRouteAuth || shouldRunCustomRouteFGA) {
|
|
698
|
+
const serverRoute = {
|
|
699
|
+
method: matchedRoute?.route.method ?? requestMethod,
|
|
700
|
+
path: matchedRoute?.route.path ?? path,
|
|
701
|
+
responseType: "json",
|
|
702
|
+
handler: async () => {},
|
|
703
|
+
requiresAuth: matchedRoute?.route.requiresAuth,
|
|
704
|
+
requiresPermission: matchedRoute?.route.requiresPermission,
|
|
705
|
+
fga: matchedRoute?.route.fga
|
|
706
|
+
};
|
|
707
|
+
if (shouldRunCustomRouteAuth) {
|
|
708
|
+
const authError = await this.checkRouteAuth(serverRoute, {
|
|
709
|
+
path,
|
|
710
|
+
method: requestMethod,
|
|
711
|
+
getHeader: (name) => ctx.request.headers.get(name) || void 0,
|
|
712
|
+
getQuery: (name) => ctx.query?.[name],
|
|
713
|
+
requestContext: ctx.requestContext,
|
|
714
|
+
request: ctx.request,
|
|
715
|
+
buildAuthorizeContext: () => ctx
|
|
716
|
+
});
|
|
717
|
+
if (authError) return new Response(JSON.stringify({ error: authError.error }), {
|
|
718
|
+
status: authError.status,
|
|
719
|
+
headers: {
|
|
720
|
+
"Content-Type": "application/json",
|
|
721
|
+
...authError.headers
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
if (this.mastra.getServer()?.auth) {
|
|
725
|
+
const hasPermission = await loadHasPermission();
|
|
726
|
+
if (hasPermission) {
|
|
727
|
+
const userPermissions = ctx.requestContext.get("userPermissions");
|
|
728
|
+
const permissionError = this.checkRoutePermission(serverRoute, userPermissions, hasPermission);
|
|
729
|
+
if (permissionError) return new Response(JSON.stringify({
|
|
730
|
+
error: permissionError.error,
|
|
731
|
+
message: permissionError.message
|
|
732
|
+
}), {
|
|
733
|
+
status: permissionError.status,
|
|
734
|
+
headers: { "Content-Type": "application/json" }
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const fgaError = await (0, _mastra_server_server_adapter.checkRouteFGA)(this.mastra, serverRoute, ctx.requestContext, {
|
|
740
|
+
...matchedRoute?.params ?? {},
|
|
741
|
+
...ctx.query,
|
|
742
|
+
...typeof ctx.body === "object" && ctx.body !== null ? ctx.body : {}
|
|
743
|
+
});
|
|
744
|
+
if (fgaError) return new Response(JSON.stringify({
|
|
745
|
+
error: fgaError.error,
|
|
746
|
+
message: fgaError.message
|
|
747
|
+
}), {
|
|
748
|
+
status: fgaError.status,
|
|
749
|
+
headers: { "Content-Type": "application/json" }
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const headers = {};
|
|
753
|
+
ctx.request.headers.forEach((value, key) => {
|
|
754
|
+
headers[key] = value;
|
|
755
|
+
});
|
|
756
|
+
const response = await this.handleCustomRouteRequest(ctx.request.url, ctx.request.method, headers, ctx.body, ctx.requestContext, ctx.request.signal);
|
|
757
|
+
if (!response) return new Response(JSON.stringify({ error: "Not Found" }), {
|
|
758
|
+
status: 404,
|
|
759
|
+
headers: { "Content-Type": "application/json" }
|
|
760
|
+
});
|
|
761
|
+
return response;
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
registerContextMiddleware() {
|
|
766
|
+
this.app.derive(this.createContextMiddleware());
|
|
767
|
+
}
|
|
768
|
+
registerAuthMiddleware() {
|
|
769
|
+
this.app.onError((ctx) => {
|
|
770
|
+
const error = ctx.error;
|
|
771
|
+
const status = error?.status ?? error?.code ?? 500;
|
|
772
|
+
const message = error?.message ?? "Internal server error";
|
|
773
|
+
const httpStatus = typeof status === "number" && status >= 400 && status < 500 ? status : 500;
|
|
774
|
+
return new Response(JSON.stringify({
|
|
775
|
+
error: httpStatus < 500 ? message : "Internal server error",
|
|
776
|
+
...httpStatus < 500 ? { issues: [] } : {}
|
|
777
|
+
}), {
|
|
778
|
+
status: httpStatus,
|
|
779
|
+
headers: { "Content-Type": "application/json" }
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
registerHttpLoggingMiddleware() {
|
|
784
|
+
if (!this.httpLoggingConfig?.enabled) return;
|
|
785
|
+
this.app.derive(async (ctx) => {
|
|
786
|
+
const path = new URL(ctx.request.url).pathname;
|
|
787
|
+
if (!this.shouldLogRequest(path)) return {};
|
|
788
|
+
return { httpLogStart: Date.now() };
|
|
789
|
+
});
|
|
790
|
+
this.app.onAfterHandle((ctx) => {
|
|
791
|
+
const path = new URL(ctx.request.url).pathname;
|
|
792
|
+
if (!this.shouldLogRequest(path)) return;
|
|
793
|
+
const duration = Date.now() - (ctx.httpLogStart ?? Date.now());
|
|
794
|
+
const method = ctx.request.method;
|
|
795
|
+
const status = ctx.response?.status ?? ctx.set?.status ?? 200;
|
|
796
|
+
const level = this.httpLoggingConfig?.level || "info";
|
|
797
|
+
const logData = {
|
|
798
|
+
method,
|
|
799
|
+
path,
|
|
800
|
+
status,
|
|
801
|
+
duration: `${duration}ms`
|
|
802
|
+
};
|
|
803
|
+
if (this.httpLoggingConfig?.includeQueryParams) logData.query = ctx.query || {};
|
|
804
|
+
if (this.httpLoggingConfig?.includeHeaders) {
|
|
805
|
+
const headers = Object.fromEntries(ctx.request.headers.entries());
|
|
806
|
+
(this.httpLoggingConfig.redactHeaders || []).forEach((h) => {
|
|
807
|
+
const key = h.toLowerCase();
|
|
808
|
+
if (headers[key] !== void 0) headers[key] = "[REDACTED]";
|
|
809
|
+
});
|
|
810
|
+
logData.headers = headers;
|
|
811
|
+
}
|
|
812
|
+
this.logger[level](`${method} ${path} ${status} ${duration}ms`, logData);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
//#endregion
|
|
817
|
+
exports.MastraServer = MastraServer;
|
|
818
|
+
exports.clearMastraOpenAPICache = clearMastraOpenAPICache;
|
|
819
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
820
|
+
exports.getMastraOpenAPIDoc = getMastraOpenAPIDoc;
|
|
821
|
+
|
|
822
|
+
//# sourceMappingURL=index.cjs.map
|