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