@mastra/fastify 1.4.11-alpha.0 → 1.4.11-alpha.2

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 CHANGED
@@ -1,797 +1,684 @@
1
- import { Busboy } from '@fastify/busboy';
2
- import { MastraServer as MastraServer$1, redactStreamChunk, serializeStreamChunk, normalizeQueryParams, isZodError, checkRouteFGA } from '@mastra/server/server-adapter';
3
- import { RequestContext } from '@mastra/core/request-context';
4
- import { coreAuthMiddleware } from '@mastra/server/auth';
5
-
6
- // src/index.ts
7
- function toWebRequest(request) {
8
- const protocol = request.protocol || "http";
9
- const host = request.headers.host || "localhost";
10
- const url = `${protocol}://${host}${request.url}`;
11
- const headers = new Headers();
12
- for (const [key, value] of Object.entries(request.headers)) {
13
- if (!value) continue;
14
- if (Array.isArray(value)) {
15
- value.forEach((v) => headers.append(key, v));
16
- } else {
17
- headers.set(key, value);
18
- }
19
- }
20
- return new globalThis.Request(url, {
21
- method: request.method,
22
- headers
23
- });
1
+ import { Busboy } from "@fastify/busboy";
2
+ import { MastraServer as MastraServer$1, checkRouteFGA, isZodError, normalizeQueryParams, redactStreamChunk, serializeStreamChunk } from "@mastra/server/server-adapter";
3
+ import { RequestContext } from "@mastra/core/request-context";
4
+ import { coreAuthMiddleware } from "@mastra/server/auth";
5
+ //#region src/auth-middleware.ts
6
+ function toWebRequest$1(request) {
7
+ const url = `${request.protocol || "http"}://${request.headers.host || "localhost"}${request.url}`;
8
+ const headers = new Headers();
9
+ for (const [key, value] of Object.entries(request.headers)) {
10
+ if (!value) continue;
11
+ if (Array.isArray(value)) value.forEach((v) => headers.append(key, v));
12
+ else headers.set(key, value);
13
+ }
14
+ return new globalThis.Request(url, {
15
+ method: request.method,
16
+ headers
17
+ });
24
18
  }
25
- function createAuthMiddleware({
26
- mastra,
27
- requiresAuth = true
28
- }) {
29
- return async (request, reply) => {
30
- if (!requiresAuth) {
31
- return;
32
- }
33
- const authConfig = mastra.getServer()?.auth;
34
- if (!authConfig) {
35
- return;
36
- }
37
- request.requestContext ??= new RequestContext();
38
- request.mastra ??= mastra;
39
- const path = String(request.url.split("?")[0] || "/");
40
- const method = String(request.method || "GET");
41
- const customRouteAuthConfig = new Map(request.customRouteAuthConfig ?? []);
42
- customRouteAuthConfig.set(`${method}:${path}`, true);
43
- const authHeader = request.headers.authorization;
44
- let token = authHeader ? authHeader.replace("Bearer ", "") : null;
45
- const query = request.query;
46
- if (!token && query.apiKey) {
47
- token = query.apiKey || null;
48
- }
49
- const result = await coreAuthMiddleware({
50
- path,
51
- method,
52
- getHeader: (name) => request.headers[name.toLowerCase()],
53
- mastra,
54
- authConfig,
55
- customRouteAuthConfig,
56
- requestContext: request.requestContext,
57
- rawRequest: toWebRequest(request),
58
- token,
59
- buildAuthorizeContext: () => toWebRequest(request)
60
- });
61
- if (result.action === "error") {
62
- return reply.status(result.status).send(result.body);
63
- }
64
- };
19
+ function createAuthMiddleware({ mastra, requiresAuth = true }) {
20
+ return async (request, reply) => {
21
+ if (!requiresAuth) return;
22
+ const authConfig = mastra.getServer()?.auth;
23
+ if (!authConfig) return;
24
+ request.requestContext ??= new RequestContext();
25
+ request.mastra ??= mastra;
26
+ const path = String(request.url.split("?")[0] || "/");
27
+ const method = String(request.method || "GET");
28
+ const customRouteAuthConfig = new Map(request.customRouteAuthConfig ?? []);
29
+ customRouteAuthConfig.set(`${method}:${path}`, true);
30
+ const authHeader = request.headers.authorization;
31
+ let token = authHeader ? authHeader.replace("Bearer ", "") : null;
32
+ const query = request.query;
33
+ if (!token && query.apiKey) token = query.apiKey || null;
34
+ const result = await coreAuthMiddleware({
35
+ path,
36
+ method,
37
+ getHeader: (name) => request.headers[name.toLowerCase()],
38
+ mastra,
39
+ authConfig,
40
+ customRouteAuthConfig,
41
+ requestContext: request.requestContext,
42
+ rawRequest: toWebRequest$1(request),
43
+ token,
44
+ buildAuthorizeContext: () => toWebRequest$1(request)
45
+ });
46
+ if (result.action === "error") return reply.status(result.status).send(result.body);
47
+ };
65
48
  }
66
-
67
- // src/index.ts
68
- var _hasPermissionPromise;
49
+ //#endregion
50
+ //#region src/index.ts
51
+ let _hasPermissionPromise;
69
52
  function loadHasPermission() {
70
- if (!_hasPermissionPromise) {
71
- _hasPermissionPromise = import('@mastra/core/auth/ee').then((m) => m.hasPermission).catch(() => {
72
- console.error(
73
- "[@mastra/fastify] Auth features require @mastra/core >= 1.6.0. Please upgrade: npm install @mastra/core@latest"
74
- );
75
- return void 0;
76
- });
77
- }
78
- return _hasPermissionPromise;
53
+ if (!_hasPermissionPromise) _hasPermissionPromise = import("@mastra/core/auth/ee").then((m) => m.hasPermission).catch(() => {
54
+ console.error("[@mastra/fastify] Auth features require @mastra/core >= 1.6.0. Please upgrade: npm install @mastra/core@latest");
55
+ });
56
+ return _hasPermissionPromise;
79
57
  }
80
- function toWebRequest2(request) {
81
- const protocol = request.protocol || "http";
82
- const host = request.headers.host || "localhost";
83
- const url = `${protocol}://${host}${request.url}`;
84
- const headers = new Headers();
85
- for (const [key, value] of Object.entries(request.headers)) {
86
- if (value) {
87
- if (Array.isArray(value)) {
88
- value.forEach((v) => headers.append(key, v));
89
- } else {
90
- headers.set(key, value);
91
- }
92
- }
93
- }
94
- return new globalThis.Request(url, {
95
- method: request.method,
96
- headers
97
- });
58
+ /**
59
+ * Convert Fastify request to Web API Request for cookie-based auth providers.
60
+ */
61
+ function toWebRequest(request) {
62
+ const url = `${request.protocol || "http"}://${request.headers.host || "localhost"}${request.url}`;
63
+ const headers = new Headers();
64
+ for (const [key, value] of Object.entries(request.headers)) if (value) if (Array.isArray(value)) value.forEach((v) => headers.append(key, v));
65
+ else headers.set(key, value);
66
+ return new globalThis.Request(url, {
67
+ method: request.method,
68
+ headers
69
+ });
98
70
  }
99
71
  function isRequestAborted(rawRequest) {
100
- return rawRequest.aborted || rawRequest.readableAborted || !rawRequest.complete;
72
+ return rawRequest.aborted || rawRequest.readableAborted || !rawRequest.complete;
101
73
  }
102
74
  var MastraServer = class extends MastraServer$1 {
103
- createContextMiddleware() {
104
- return async (request, reply) => {
105
- let bodyRequestContext;
106
- let paramsRequestContext;
107
- if (request.method === "POST" || request.method === "PUT") {
108
- const contentType = request.headers["content-type"];
109
- if (contentType?.includes("application/json") && request.body) {
110
- const body = request.body;
111
- if (body.requestContext) {
112
- bodyRequestContext = body.requestContext;
113
- }
114
- }
115
- }
116
- if (request.method === "GET") {
117
- try {
118
- const query = request.query;
119
- const encodedRequestContext = query.requestContext;
120
- if (typeof encodedRequestContext === "string") {
121
- try {
122
- paramsRequestContext = JSON.parse(encodedRequestContext);
123
- } catch {
124
- try {
125
- const json = Buffer.from(encodedRequestContext, "base64").toString("utf-8");
126
- paramsRequestContext = JSON.parse(json);
127
- } catch {
128
- }
129
- }
130
- }
131
- } catch {
132
- }
133
- }
134
- const requestContext = this.mergeRequestContext({ paramsRequestContext, bodyRequestContext });
135
- this.applyRequestMetadataToContext({
136
- requestContext,
137
- getHeader: (name) => {
138
- const value = request.headers[name.toLowerCase()];
139
- return Array.isArray(value) ? value[0] : value;
140
- }
141
- });
142
- request.requestContext = requestContext;
143
- request.mastra = this.mastra;
144
- request.registeredTools = this.tools || {};
145
- if (this.taskStore) {
146
- request.taskStore = this.taskStore;
147
- }
148
- request.customRouteAuthConfig = this.customRouteAuthConfig;
149
- const controller = new AbortController();
150
- request.raw.on("close", () => {
151
- if (isRequestAborted(request.raw)) {
152
- controller.abort();
153
- }
154
- });
155
- reply.raw.on("close", () => {
156
- if (!reply.raw.writableEnded) {
157
- controller.abort();
158
- }
159
- });
160
- request.abortSignal = controller.signal;
161
- };
162
- }
163
- async stream(route, reply, result, request) {
164
- const rawHeaders = reply.getHeaders();
165
- const existingHeaders = {};
166
- for (const [key, value] of Object.entries(rawHeaders)) {
167
- if (value === void 0) continue;
168
- const lowerKey = key.toLowerCase();
169
- if (lowerKey === "content-length" || lowerKey === "transfer-encoding") continue;
170
- existingHeaders[key] = value;
171
- }
172
- reply.hijack();
173
- const streamFormat = route.streamFormat || "stream";
174
- const sseHeaders = streamFormat === "sse" ? {
175
- "Content-Type": "text/event-stream",
176
- "Cache-Control": "no-cache",
177
- Connection: "keep-alive",
178
- "X-Accel-Buffering": "no"
179
- } : {
180
- "Content-Type": "text/plain"
181
- };
182
- reply.raw.writeHead(200, {
183
- ...existingHeaders,
184
- ...sseHeaders,
185
- "Transfer-Encoding": "chunked"
186
- });
187
- if (streamFormat === "sse" && route.sseFlushOnConnect) {
188
- reply.raw.write(": connected\n\n");
189
- }
190
- const readableStream = result instanceof ReadableStream ? result : result.fullStream;
191
- const reader = readableStream.getReader();
192
- let readerCanceled = false;
193
- const cancelReader = (reason) => {
194
- if (readerCanceled) return;
195
- readerCanceled = true;
196
- void reader.cancel(reason);
197
- };
198
- const cancelReaderOnResponseClose = () => cancelReader("request aborted");
199
- const cancelReaderOnRequestClose = () => {
200
- if (request && isRequestAborted(request.raw)) {
201
- cancelReader("request aborted");
202
- }
203
- };
204
- reply.raw.on("close", cancelReaderOnResponseClose);
205
- request?.raw.on("close", cancelReaderOnRequestClose);
206
- try {
207
- while (true) {
208
- const { done, value } = await reader.read();
209
- if (done) break;
210
- if (value) {
211
- if (streamFormat === "sse" && typeof value === "string" && value.startsWith(":")) {
212
- reply.raw.write(value);
213
- continue;
214
- }
215
- const shouldRedact = this.streamOptions?.redact ?? true;
216
- const outputValue = shouldRedact ? redactStreamChunk(value) : value;
217
- const serialized = serializeStreamChunk(outputValue);
218
- if (!serialized.ok) {
219
- this.mastra.getLogger()?.error("Failed to serialize stream chunk, skipping", {
220
- path: route.path,
221
- chunkType: outputValue?.type,
222
- error: serialized.error.message
223
- });
224
- continue;
225
- }
226
- if (streamFormat === "sse") {
227
- reply.raw.write(`data: ${serialized.json}
228
-
229
- `);
230
- } else {
231
- reply.raw.write(serialized.json + "");
232
- }
233
- }
234
- }
235
- } catch (error) {
236
- this.mastra.getLogger()?.error("Error in stream processing", {
237
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
238
- });
239
- } finally {
240
- reply.raw.off("close", cancelReaderOnResponseClose);
241
- request?.raw.off("close", cancelReaderOnRequestClose);
242
- if (!reply.raw.writableEnded && !reply.raw.destroyed) {
243
- reply.raw.end();
244
- }
245
- }
246
- }
247
- async getParams(route, request) {
248
- const urlParams = request.params || {};
249
- const queryParams = normalizeQueryParams(request.query || {});
250
- let body;
251
- let bodyParseError;
252
- if (route.method === "POST" || route.method === "PUT" || route.method === "PATCH" || route.method === "DELETE") {
253
- const contentType = request.headers["content-type"] || "";
254
- if (contentType.includes("multipart/form-data")) {
255
- try {
256
- const maxFileSize = route.maxBodySize ?? this.bodyLimitOptions?.maxSize;
257
- body = await this.parseMultipartFormData(request, maxFileSize);
258
- } catch (error) {
259
- this.mastra.getLogger()?.error("Failed to parse multipart form data", {
260
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
261
- });
262
- if (error instanceof Error && error.message.toLowerCase().includes("size")) {
263
- throw error;
264
- }
265
- bodyParseError = {
266
- message: error instanceof Error ? error.message : "Failed to parse multipart form data"
267
- };
268
- }
269
- } else {
270
- body = request.body;
271
- }
272
- }
273
- return { urlParams, queryParams, body, bodyParseError };
274
- }
275
- /**
276
- * Parse multipart/form-data using @fastify/busboy.
277
- * Converts file uploads to Buffers and parses JSON field values.
278
- *
279
- * @param request - The Fastify request object
280
- * @param maxFileSize - Optional maximum file size in bytes
281
- */
282
- parseMultipartFormData(request, maxFileSize) {
283
- return new Promise((resolve, reject) => {
284
- const result = {};
285
- const busboy = new Busboy({
286
- headers: {
287
- "content-type": request.headers["content-type"]
288
- },
289
- limits: maxFileSize ? { fileSize: maxFileSize } : void 0
290
- });
291
- busboy.on(
292
- "file",
293
- (fieldname, file, _filename, _encoding, _mimetype) => {
294
- const chunks = [];
295
- let limitExceeded = false;
296
- file.on("data", (chunk) => {
297
- chunks.push(chunk);
298
- });
299
- file.on("limit", () => {
300
- limitExceeded = true;
301
- file.resume();
302
- reject(new Error(`File size limit exceeded${maxFileSize ? ` (max: ${maxFileSize} bytes)` : ""}`));
303
- });
304
- file.on("end", () => {
305
- if (!limitExceeded) {
306
- result[fieldname] = Buffer.concat(chunks);
307
- }
308
- });
309
- }
310
- );
311
- busboy.on("field", (fieldname, value) => {
312
- try {
313
- result[fieldname] = JSON.parse(value);
314
- } catch {
315
- result[fieldname] = value;
316
- }
317
- });
318
- busboy.on("finish", () => {
319
- resolve(result);
320
- });
321
- busboy.on("error", (error) => {
322
- reject(error);
323
- });
324
- request.raw.pipe(busboy);
325
- });
326
- }
327
- async sendResponse(route, reply, result, request, prefix) {
328
- const resolvedPrefix = prefix ?? this.prefix ?? "";
329
- if (result && typeof result === "object" && "__refreshHeaders" in result) {
330
- const refreshHeaders = result.__refreshHeaders;
331
- for (const [key, value] of Object.entries(refreshHeaders)) {
332
- reply.header(key, value);
333
- }
334
- delete result.__refreshHeaders;
335
- }
336
- if (route.responseType === "json") {
337
- await reply.send(result);
338
- } else if (route.responseType === "stream") {
339
- await this.stream(route, reply, result, request);
340
- } else if (route.responseType === "datastream-response") {
341
- const fetchResponse = result;
342
- fetchResponse.headers.forEach((value, key) => reply.header(key, value));
343
- reply.status(fetchResponse.status);
344
- if (fetchResponse.body) {
345
- const reader = fetchResponse.body.getReader();
346
- let readerCanceled = false;
347
- const cancelReader = (reason) => {
348
- if (readerCanceled) return;
349
- readerCanceled = true;
350
- void reader.cancel(reason);
351
- };
352
- const cancelReaderOnResponseClose = () => cancelReader("request aborted");
353
- const cancelReaderOnRequestClose = () => {
354
- if (request && isRequestAborted(request.raw)) {
355
- cancelReader("request aborted");
356
- }
357
- };
358
- const onResError = (err) => {
359
- this.mastra.getLogger()?.error("Error writing datastream response", {
360
- error: err instanceof Error ? { message: err.message, stack: err.stack } : err
361
- });
362
- cancelReader("response write error");
363
- };
364
- reply.raw.once("error", onResError);
365
- reply.raw.on("close", cancelReaderOnResponseClose);
366
- request?.raw.on("close", cancelReaderOnRequestClose);
367
- try {
368
- while (true) {
369
- const { done, value } = await reader.read();
370
- if (done) break;
371
- reply.raw.write(value);
372
- }
373
- } catch (error) {
374
- this.mastra.getLogger()?.error("Error in datastream processing", {
375
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
376
- });
377
- } finally {
378
- reply.raw.off("error", onResError);
379
- reply.raw.off("close", cancelReaderOnResponseClose);
380
- request?.raw.off("close", cancelReaderOnRequestClose);
381
- if (!reply.raw.writableEnded && !reply.raw.destroyed) {
382
- reply.raw.end();
383
- }
384
- }
385
- } else {
386
- reply.raw.end();
387
- }
388
- } else if (route.responseType === "mcp-http") {
389
- if (!request) {
390
- await reply.status(500).send({ error: "Request object required for MCP transport" });
391
- return;
392
- }
393
- const { server, httpPath, mcpOptions: routeMcpOptions } = result;
394
- try {
395
- reply.hijack();
396
- const rawReq = request.raw;
397
- if (request.body !== void 0) {
398
- rawReq.body = request.body;
399
- }
400
- const options = { ...this.mcpOptions, ...routeMcpOptions };
401
- await server.startHTTP({
402
- url: new URL(request.url, `http://${request.headers.host}`),
403
- httpPath: `${resolvedPrefix}${httpPath}`,
404
- req: rawReq,
405
- res: reply.raw,
406
- options: Object.keys(options).length > 0 ? options : void 0
407
- });
408
- } catch {
409
- if (!reply.raw.headersSent) {
410
- reply.raw.writeHead(500, { "Content-Type": "application/json" });
411
- reply.raw.end(
412
- JSON.stringify({
413
- jsonrpc: "2.0",
414
- error: { code: -32603, message: "Internal server error" },
415
- id: null
416
- })
417
- );
418
- }
419
- }
420
- } else if (route.responseType === "mcp-sse") {
421
- if (!request) {
422
- await reply.status(500).send({ error: "Request object required for MCP transport" });
423
- return;
424
- }
425
- const { server, ssePath, messagePath } = result;
426
- try {
427
- reply.hijack();
428
- const rawReq = request.raw;
429
- if (request.body !== void 0) {
430
- rawReq.body = request.body;
431
- }
432
- await server.startSSE({
433
- url: new URL(request.url, `http://${request.headers.host}`),
434
- ssePath: `${resolvedPrefix}${ssePath}`,
435
- messagePath: `${resolvedPrefix}${messagePath}`,
436
- req: rawReq,
437
- res: reply.raw
438
- });
439
- } catch {
440
- if (!reply.raw.headersSent) {
441
- reply.raw.writeHead(500, { "Content-Type": "application/json" });
442
- reply.raw.end(JSON.stringify({ error: "Error handling MCP SSE request" }));
443
- }
444
- }
445
- } else {
446
- reply.status(500);
447
- }
448
- }
449
- async registerRoute(app, route, { prefix: prefixParam } = {}) {
450
- const prefix = prefixParam ?? this.prefix ?? "";
451
- const fullPath = `${prefix}${route.path}`;
452
- const fastifyPath = fullPath;
453
- const handler = async (request, reply) => {
454
- const authError = await this.checkRouteAuth(route, {
455
- path: String(request.url.split("?")[0] || "/"),
456
- method: String(request.method || "GET"),
457
- getHeader: (name) => request.headers[name.toLowerCase()],
458
- getQuery: (name) => request.query[name],
459
- requestContext: request.requestContext,
460
- request: toWebRequest2(request),
461
- buildAuthorizeContext: () => toWebRequest2(request)
462
- });
463
- if (authError) {
464
- if (authError.headers) {
465
- for (const [key, value] of Object.entries(authError.headers)) {
466
- void reply.header(key, value);
467
- }
468
- }
469
- if (authError.error) {
470
- return reply.status(authError.status).send({ error: authError.error });
471
- }
472
- }
473
- const params = await this.getParams(route, request);
474
- if (params.bodyParseError) {
475
- return reply.status(400).send({
476
- error: "Invalid request body",
477
- issues: [{ field: "body", message: params.bodyParseError.message }]
478
- });
479
- }
480
- if (params.queryParams) {
481
- try {
482
- params.queryParams = await this.parseQueryParams(route, params.queryParams);
483
- } catch (error) {
484
- this.mastra.getLogger()?.error("Error parsing query params", {
485
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
486
- });
487
- if (isZodError(error)) {
488
- const { status, body } = this.resolveValidationError(route, error, "query");
489
- return reply.status(status).send(body);
490
- }
491
- return reply.status(400).send({
492
- error: "Invalid query parameters",
493
- issues: [{ field: "unknown", message: error instanceof Error ? error.message : "Unknown error" }]
494
- });
495
- }
496
- }
497
- if (params.body) {
498
- try {
499
- params.body = await this.parseBody(route, params.body);
500
- } catch (error) {
501
- this.mastra.getLogger()?.error("Error parsing body", {
502
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
503
- });
504
- if (isZodError(error)) {
505
- const { status, body } = this.resolveValidationError(route, error, "body");
506
- return reply.status(status).send(body);
507
- }
508
- return reply.status(400).send({
509
- error: "Invalid request body",
510
- issues: [{ field: "unknown", message: error instanceof Error ? error.message : "Unknown error" }]
511
- });
512
- }
513
- }
514
- if (params.urlParams) {
515
- try {
516
- params.urlParams = await this.parsePathParams(route, params.urlParams);
517
- } catch (error) {
518
- this.mastra.getLogger()?.error("Error parsing path params", {
519
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error
520
- });
521
- if (isZodError(error)) {
522
- const { status, body } = this.resolveValidationError(route, error, "path");
523
- return reply.status(status).send(body);
524
- }
525
- return reply.status(400).send({
526
- error: "Invalid path parameters",
527
- issues: [{ field: "unknown", message: error instanceof Error ? error.message : "Unknown error" }]
528
- });
529
- }
530
- }
531
- const handlerParams = {
532
- ...params.urlParams,
533
- ...params.queryParams,
534
- ...typeof params.body === "object" ? params.body : {},
535
- requestContext: request.requestContext,
536
- mastra: this.mastra,
537
- registeredTools: request.registeredTools,
538
- taskStore: request.taskStore,
539
- abortSignal: request.abortSignal,
540
- routePrefix: prefix,
541
- request: toWebRequest2(request)
542
- };
543
- const requestContext = request.requestContext;
544
- const hasAuth = this.mastra.getStudio?.()?.auth || this.mastra.getServer()?.auth;
545
- if (hasAuth) {
546
- const hasPermission = await loadHasPermission();
547
- if (hasPermission) {
548
- const userPermissions = requestContext.get("mastra__userPermissions");
549
- const permissionError = this.checkRoutePermission(route, userPermissions, hasPermission, requestContext);
550
- if (permissionError) {
551
- return reply.status(permissionError.status).send({
552
- error: permissionError.error,
553
- message: permissionError.message
554
- });
555
- }
556
- }
557
- }
558
- const fgaError = await checkRouteFGA(this.mastra, route, requestContext, {
559
- ...params.urlParams,
560
- ...params.queryParams,
561
- ...typeof params.body === "object" ? params.body : {}
562
- });
563
- if (fgaError) {
564
- return reply.status(fgaError.status).send({ error: fgaError.error, message: fgaError.message });
565
- }
566
- try {
567
- const result = await route.handler(handlerParams);
568
- await this.sendResponse(route, reply, result, request, prefix);
569
- } catch (error) {
570
- const httpStatus = error && typeof error === "object" && "status" in error ? error.status : void 0;
571
- const isClientError = typeof httpStatus === "number" && httpStatus >= 400 && httpStatus < 500;
572
- if (!isClientError) {
573
- this.mastra.getLogger()?.error("Error calling handler", {
574
- error: error instanceof Error ? { message: error.message, stack: error.stack } : error,
575
- path: route.path,
576
- method: route.method
577
- });
578
- }
579
- let status = 500;
580
- if (error && typeof error === "object") {
581
- if ("status" in error) {
582
- status = error.status;
583
- } else if ("details" in error && error.details && typeof error.details === "object" && "status" in error.details) {
584
- status = error.details.status;
585
- }
586
- }
587
- await reply.status(status).send({ error: error instanceof Error ? error.message : "Unknown error" });
588
- }
589
- };
590
- const shouldApplyBodyLimit = this.bodyLimitOptions && ["POST", "PUT", "PATCH"].includes(route.method.toUpperCase());
591
- const maxSize = route.maxBodySize ?? this.bodyLimitOptions?.maxSize;
592
- const config = shouldApplyBodyLimit && maxSize ? { bodyLimit: maxSize } : void 0;
593
- if (route.method.toUpperCase() === "ALL") {
594
- const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"];
595
- for (const method of methods) {
596
- try {
597
- app.route({
598
- method,
599
- url: fastifyPath,
600
- handler,
601
- config
602
- });
603
- } catch (err) {
604
- if (err instanceof Error && err.message.includes("already declared")) {
605
- continue;
606
- }
607
- throw err;
608
- }
609
- }
610
- } else {
611
- app.route({
612
- method: route.method,
613
- url: fastifyPath,
614
- handler,
615
- config
616
- });
617
- }
618
- }
619
- async registerCustomApiRoutes() {
620
- if (!await this.buildCustomRouteHandler()) return;
621
- const routes = this.customApiRoutes ?? this.mastra.getServer()?.apiRoutes ?? [];
622
- for (const route of routes) {
623
- const serverRoute = {
624
- method: route.method,
625
- path: route.path,
626
- responseType: "json",
627
- handler: async () => {
628
- },
629
- requiresAuth: route.requiresAuth,
630
- requiresPermission: route.requiresPermission,
631
- fga: route.fga
632
- };
633
- const fastifyHandler = async (request, reply) => {
634
- const authError = await this.checkRouteAuth(serverRoute, {
635
- path: String(request.url.split("?")[0] || "/"),
636
- method: String(request.method || "GET"),
637
- getHeader: (name) => request.headers[name.toLowerCase()],
638
- getQuery: (name) => request.query[name],
639
- requestContext: request.requestContext,
640
- request: toWebRequest2(request),
641
- buildAuthorizeContext: () => toWebRequest2(request)
642
- });
643
- if (authError) {
644
- if (authError.headers) {
645
- for (const [key, value] of Object.entries(authError.headers)) {
646
- void reply.header(key, value);
647
- }
648
- }
649
- if (authError.error) {
650
- return reply.status(authError.status).send({ error: authError.error });
651
- }
652
- }
653
- const requestContext = request.requestContext;
654
- const hasAuth = this.mastra.getStudio?.()?.auth || this.mastra.getServer()?.auth;
655
- if (hasAuth) {
656
- let hasPermission;
657
- try {
658
- ({ hasPermission } = await import('@mastra/core/auth/ee'));
659
- } catch {
660
- console.error(
661
- "[@mastra/fastify] Auth features require @mastra/core >= 1.6.0. Please upgrade: npm install @mastra/core@latest"
662
- );
663
- }
664
- if (hasPermission) {
665
- const userPermissions = requestContext.get("mastra__userPermissions");
666
- const permissionError = this.checkRoutePermission(
667
- serverRoute,
668
- userPermissions,
669
- hasPermission,
670
- requestContext
671
- );
672
- if (permissionError) {
673
- return reply.status(permissionError.status).send({
674
- error: permissionError.error,
675
- message: permissionError.message
676
- });
677
- }
678
- }
679
- }
680
- const fgaError = await checkRouteFGA(this.mastra, serverRoute, requestContext, {
681
- ...request.params,
682
- ...request.query,
683
- ...typeof request.body === "object" && request.body !== null ? request.body : {}
684
- });
685
- if (fgaError) {
686
- return reply.status(fgaError.status).send({ error: fgaError.error, message: fgaError.message });
687
- }
688
- const response = await this.handleCustomRouteRequest(
689
- `http://${request.headers.host}${request.url}`,
690
- request.method,
691
- request.headers,
692
- request.body,
693
- request.requestContext,
694
- request.abortSignal
695
- );
696
- if (!response) {
697
- reply.status(404).send({ error: "Not Found" });
698
- return;
699
- }
700
- const existingHeaders = reply.getHeaders();
701
- for (const [key, value] of Object.entries(existingHeaders)) {
702
- if (value === void 0) continue;
703
- const lowerKey = key.toLowerCase();
704
- if (lowerKey === "content-length" || lowerKey === "transfer-encoding") continue;
705
- const isSetCookie = lowerKey === "set-cookie";
706
- if (!isSetCookie && response.headers.has(key)) continue;
707
- if (Array.isArray(value)) {
708
- for (const item of value) response.headers.append(key, String(item));
709
- } else if (isSetCookie) {
710
- response.headers.append(key, String(value));
711
- } else {
712
- response.headers.set(key, String(value));
713
- }
714
- }
715
- reply.hijack();
716
- await this.writeCustomRouteResponse(response, reply.raw, request.abortSignal);
717
- };
718
- if (route.method === "ALL") {
719
- const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"];
720
- for (const method of methods) {
721
- this.app.route({ method, url: route.path, handler: fastifyHandler });
722
- }
723
- } else {
724
- this.app.route({
725
- method: route.method,
726
- url: route.path,
727
- handler: fastifyHandler
728
- });
729
- }
730
- }
731
- }
732
- registerContextMiddleware() {
733
- this.app.removeContentTypeParser("application/json");
734
- this.app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
735
- try {
736
- if (!body || typeof body === "string" && body.trim() === "") {
737
- done(null, void 0);
738
- return;
739
- }
740
- const parsed = JSON.parse(body);
741
- done(null, parsed);
742
- } catch (err) {
743
- done(err, void 0);
744
- }
745
- });
746
- this.app.addContentTypeParser("multipart/form-data", (_request, _payload, done) => {
747
- done(null, void 0);
748
- });
749
- this.app.addHook("preHandler", this.createContextMiddleware());
750
- }
751
- registerAuthMiddleware() {
752
- }
753
- registerHttpLoggingMiddleware() {
754
- if (!this.httpLoggingConfig?.enabled) {
755
- return;
756
- }
757
- this.app.addHook("onRequest", async (request, reply) => {
758
- const urlPath = request.url.split("?")[0];
759
- if (!this.shouldLogRequest(urlPath)) {
760
- return;
761
- }
762
- const start = Date.now();
763
- const method = request.method;
764
- const path = urlPath;
765
- reply.raw.once("finish", () => {
766
- const duration = Date.now() - start;
767
- const status = reply.statusCode;
768
- const level = this.httpLoggingConfig?.level || "info";
769
- const logData = {
770
- method,
771
- path,
772
- status,
773
- duration: `${duration}ms`
774
- };
775
- if (this.httpLoggingConfig?.includeQueryParams) {
776
- logData.query = request.query;
777
- }
778
- if (this.httpLoggingConfig?.includeHeaders) {
779
- const headers = { ...request.headers };
780
- const redactHeaders = this.httpLoggingConfig.redactHeaders || [];
781
- redactHeaders.forEach((h) => {
782
- const key = h.toLowerCase();
783
- if (headers[key] !== void 0) {
784
- headers[key] = "[REDACTED]";
785
- }
786
- });
787
- logData.headers = headers;
788
- }
789
- this.logger[level](`${method} ${path} ${status} ${duration}ms`, logData);
790
- });
791
- });
792
- }
75
+ createContextMiddleware() {
76
+ return async (request, reply) => {
77
+ let bodyRequestContext;
78
+ let paramsRequestContext;
79
+ if (request.method === "POST" || request.method === "PUT") {
80
+ if (request.headers["content-type"]?.includes("application/json") && request.body) {
81
+ const body = request.body;
82
+ if (body.requestContext) bodyRequestContext = body.requestContext;
83
+ }
84
+ }
85
+ if (request.method === "GET") try {
86
+ const encodedRequestContext = request.query.requestContext;
87
+ if (typeof encodedRequestContext === "string") try {
88
+ paramsRequestContext = JSON.parse(encodedRequestContext);
89
+ } catch {
90
+ try {
91
+ const json = Buffer.from(encodedRequestContext, "base64").toString("utf-8");
92
+ paramsRequestContext = JSON.parse(json);
93
+ } catch {}
94
+ }
95
+ } catch {}
96
+ const requestContext = this.mergeRequestContext({
97
+ paramsRequestContext,
98
+ bodyRequestContext
99
+ });
100
+ this.applyRequestMetadataToContext({
101
+ requestContext,
102
+ getHeader: (name) => {
103
+ const value = request.headers[name.toLowerCase()];
104
+ return Array.isArray(value) ? value[0] : value;
105
+ }
106
+ });
107
+ request.requestContext = requestContext;
108
+ request.mastra = this.mastra;
109
+ request.registeredTools = this.tools || {};
110
+ if (this.taskStore) request.taskStore = this.taskStore;
111
+ request.customRouteAuthConfig = this.customRouteAuthConfig;
112
+ const controller = new AbortController();
113
+ request.raw.on("close", () => {
114
+ if (isRequestAborted(request.raw)) controller.abort();
115
+ });
116
+ reply.raw.on("close", () => {
117
+ if (!reply.raw.writableEnded) controller.abort();
118
+ });
119
+ request.abortSignal = controller.signal;
120
+ };
121
+ }
122
+ async stream(route, reply, result, request) {
123
+ const rawHeaders = reply.getHeaders();
124
+ const existingHeaders = {};
125
+ for (const [key, value] of Object.entries(rawHeaders)) {
126
+ if (value === void 0) continue;
127
+ const lowerKey = key.toLowerCase();
128
+ if (lowerKey === "content-length" || lowerKey === "transfer-encoding") continue;
129
+ existingHeaders[key] = value;
130
+ }
131
+ reply.hijack();
132
+ const streamFormat = route.streamFormat || "stream";
133
+ const sseHeaders = streamFormat === "sse" ? {
134
+ "Content-Type": "text/event-stream",
135
+ "Cache-Control": "no-cache",
136
+ Connection: "keep-alive",
137
+ "X-Accel-Buffering": "no"
138
+ } : { "Content-Type": "text/plain" };
139
+ reply.raw.writeHead(200, {
140
+ ...existingHeaders,
141
+ ...sseHeaders,
142
+ "Transfer-Encoding": "chunked"
143
+ });
144
+ if (streamFormat === "sse" && route.sseFlushOnConnect) reply.raw.write(": connected\n\n");
145
+ const reader = (result instanceof ReadableStream ? result : result.fullStream).getReader();
146
+ let readerCanceled = false;
147
+ const cancelReader = (reason) => {
148
+ if (readerCanceled) return;
149
+ readerCanceled = true;
150
+ reader.cancel(reason);
151
+ };
152
+ const cancelReaderOnResponseClose = () => cancelReader("request aborted");
153
+ const cancelReaderOnRequestClose = () => {
154
+ if (request && isRequestAborted(request.raw)) cancelReader("request aborted");
155
+ };
156
+ reply.raw.on("close", cancelReaderOnResponseClose);
157
+ request?.raw.on("close", cancelReaderOnRequestClose);
158
+ try {
159
+ while (true) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ if (value) {
163
+ if (streamFormat === "sse" && typeof value === "string" && value.startsWith(":")) {
164
+ reply.raw.write(value);
165
+ continue;
166
+ }
167
+ const outputValue = this.streamOptions?.redact ?? true ? redactStreamChunk(value) : value;
168
+ const serialized = serializeStreamChunk(outputValue);
169
+ if (!serialized.ok) {
170
+ this.mastra.getLogger()?.error("Failed to serialize stream chunk, skipping", {
171
+ path: route.path,
172
+ chunkType: outputValue?.type,
173
+ error: serialized.error.message
174
+ });
175
+ continue;
176
+ }
177
+ if (streamFormat === "sse") reply.raw.write(`data: ${serialized.json}\n\n`);
178
+ else reply.raw.write(serialized.json + "");
179
+ }
180
+ }
181
+ } catch (error) {
182
+ this.mastra.getLogger()?.error("Error in stream processing", { error: error instanceof Error ? {
183
+ message: error.message,
184
+ stack: error.stack
185
+ } : error });
186
+ } finally {
187
+ reply.raw.off("close", cancelReaderOnResponseClose);
188
+ request?.raw.off("close", cancelReaderOnRequestClose);
189
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
190
+ }
191
+ }
192
+ async getParams(route, request) {
193
+ const urlParams = request.params || {};
194
+ const queryParams = normalizeQueryParams(request.query || {});
195
+ let body;
196
+ let bodyParseError;
197
+ if (route.method === "POST" || route.method === "PUT" || route.method === "PATCH" || route.method === "DELETE") if ((request.headers["content-type"] || "").includes("multipart/form-data")) try {
198
+ const maxFileSize = route.maxBodySize ?? this.bodyLimitOptions?.maxSize;
199
+ body = await this.parseMultipartFormData(request, maxFileSize);
200
+ } catch (error) {
201
+ this.mastra.getLogger()?.error("Failed to parse multipart form data", { error: error instanceof Error ? {
202
+ message: error.message,
203
+ stack: error.stack
204
+ } : error });
205
+ if (error instanceof Error && error.message.toLowerCase().includes("size")) throw error;
206
+ bodyParseError = { message: error instanceof Error ? error.message : "Failed to parse multipart form data" };
207
+ }
208
+ else body = request.body;
209
+ return {
210
+ urlParams,
211
+ queryParams,
212
+ body,
213
+ bodyParseError
214
+ };
215
+ }
216
+ /**
217
+ * Parse multipart/form-data using @fastify/busboy.
218
+ * Converts file uploads to Buffers and parses JSON field values.
219
+ *
220
+ * @param request - The Fastify request object
221
+ * @param maxFileSize - Optional maximum file size in bytes
222
+ */
223
+ parseMultipartFormData(request, maxFileSize) {
224
+ return new Promise((resolve, reject) => {
225
+ const result = {};
226
+ const busboy = new Busboy({
227
+ headers: { "content-type": request.headers["content-type"] },
228
+ limits: maxFileSize ? { fileSize: maxFileSize } : void 0
229
+ });
230
+ busboy.on("file", (fieldname, file, _filename, _encoding, _mimetype) => {
231
+ const chunks = [];
232
+ let limitExceeded = false;
233
+ file.on("data", (chunk) => {
234
+ chunks.push(chunk);
235
+ });
236
+ file.on("limit", () => {
237
+ limitExceeded = true;
238
+ file.resume();
239
+ reject(/* @__PURE__ */ new Error(`File size limit exceeded${maxFileSize ? ` (max: ${maxFileSize} bytes)` : ""}`));
240
+ });
241
+ file.on("end", () => {
242
+ if (!limitExceeded) result[fieldname] = Buffer.concat(chunks);
243
+ });
244
+ });
245
+ busboy.on("field", (fieldname, value) => {
246
+ try {
247
+ result[fieldname] = JSON.parse(value);
248
+ } catch {
249
+ result[fieldname] = value;
250
+ }
251
+ });
252
+ busboy.on("finish", () => {
253
+ resolve(result);
254
+ });
255
+ busboy.on("error", (error) => {
256
+ reject(error);
257
+ });
258
+ request.raw.pipe(busboy);
259
+ });
260
+ }
261
+ async sendResponse(route, reply, result, request, prefix) {
262
+ const resolvedPrefix = prefix ?? this.prefix ?? "";
263
+ if (result && typeof result === "object" && "__refreshHeaders" in result) {
264
+ const refreshHeaders = result.__refreshHeaders;
265
+ for (const [key, value] of Object.entries(refreshHeaders)) reply.header(key, value);
266
+ delete result.__refreshHeaders;
267
+ }
268
+ if (route.responseType === "json") await reply.send(result);
269
+ else if (route.responseType === "stream") await this.stream(route, reply, result, request);
270
+ else if (route.responseType === "datastream-response") {
271
+ const fetchResponse = result;
272
+ fetchResponse.headers.forEach((value, key) => reply.header(key, value));
273
+ reply.status(fetchResponse.status);
274
+ if (fetchResponse.body) {
275
+ const reader = fetchResponse.body.getReader();
276
+ let readerCanceled = false;
277
+ const cancelReader = (reason) => {
278
+ if (readerCanceled) return;
279
+ readerCanceled = true;
280
+ reader.cancel(reason);
281
+ };
282
+ const cancelReaderOnResponseClose = () => cancelReader("request aborted");
283
+ const cancelReaderOnRequestClose = () => {
284
+ if (request && isRequestAborted(request.raw)) cancelReader("request aborted");
285
+ };
286
+ const onResError = (err) => {
287
+ this.mastra.getLogger()?.error("Error writing datastream response", { error: err instanceof Error ? {
288
+ message: err.message,
289
+ stack: err.stack
290
+ } : err });
291
+ cancelReader("response write error");
292
+ };
293
+ reply.raw.once("error", onResError);
294
+ reply.raw.on("close", cancelReaderOnResponseClose);
295
+ request?.raw.on("close", cancelReaderOnRequestClose);
296
+ try {
297
+ while (true) {
298
+ const { done, value } = await reader.read();
299
+ if (done) break;
300
+ reply.raw.write(value);
301
+ }
302
+ } catch (error) {
303
+ this.mastra.getLogger()?.error("Error in datastream processing", { error: error instanceof Error ? {
304
+ message: error.message,
305
+ stack: error.stack
306
+ } : error });
307
+ } finally {
308
+ reply.raw.off("error", onResError);
309
+ reply.raw.off("close", cancelReaderOnResponseClose);
310
+ request?.raw.off("close", cancelReaderOnRequestClose);
311
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
312
+ }
313
+ } else reply.raw.end();
314
+ } else if (route.responseType === "mcp-http") {
315
+ if (!request) {
316
+ await reply.status(500).send({ error: "Request object required for MCP transport" });
317
+ return;
318
+ }
319
+ const { server, httpPath, mcpOptions: routeMcpOptions } = result;
320
+ try {
321
+ reply.hijack();
322
+ const rawReq = request.raw;
323
+ if (request.body !== void 0) rawReq.body = request.body;
324
+ const options = {
325
+ ...this.mcpOptions,
326
+ ...routeMcpOptions
327
+ };
328
+ await server.startHTTP({
329
+ url: new URL(request.url, `http://${request.headers.host}`),
330
+ httpPath: `${resolvedPrefix}${httpPath}`,
331
+ req: rawReq,
332
+ res: reply.raw,
333
+ options: Object.keys(options).length > 0 ? options : void 0
334
+ });
335
+ } catch {
336
+ if (!reply.raw.headersSent) {
337
+ reply.raw.writeHead(500, { "Content-Type": "application/json" });
338
+ reply.raw.end(JSON.stringify({
339
+ jsonrpc: "2.0",
340
+ error: {
341
+ code: -32603,
342
+ message: "Internal server error"
343
+ },
344
+ id: null
345
+ }));
346
+ }
347
+ }
348
+ } else if (route.responseType === "mcp-sse") {
349
+ if (!request) {
350
+ await reply.status(500).send({ error: "Request object required for MCP transport" });
351
+ return;
352
+ }
353
+ const { server, ssePath, messagePath } = result;
354
+ try {
355
+ reply.hijack();
356
+ const rawReq = request.raw;
357
+ if (request.body !== void 0) rawReq.body = request.body;
358
+ await server.startSSE({
359
+ url: new URL(request.url, `http://${request.headers.host}`),
360
+ ssePath: `${resolvedPrefix}${ssePath}`,
361
+ messagePath: `${resolvedPrefix}${messagePath}`,
362
+ req: rawReq,
363
+ res: reply.raw
364
+ });
365
+ } catch {
366
+ if (!reply.raw.headersSent) {
367
+ reply.raw.writeHead(500, { "Content-Type": "application/json" });
368
+ reply.raw.end(JSON.stringify({ error: "Error handling MCP SSE request" }));
369
+ }
370
+ }
371
+ } else reply.status(500);
372
+ }
373
+ async registerRoute(app, route, { prefix: prefixParam } = {}) {
374
+ const prefix = prefixParam ?? this.prefix ?? "";
375
+ const fastifyPath = `${prefix}${route.path}`;
376
+ const handler = async (request, reply) => {
377
+ const authError = await this.checkRouteAuth(route, {
378
+ path: String(request.url.split("?")[0] || "/"),
379
+ method: String(request.method || "GET"),
380
+ getHeader: (name) => request.headers[name.toLowerCase()],
381
+ getQuery: (name) => request.query[name],
382
+ requestContext: request.requestContext,
383
+ request: toWebRequest(request),
384
+ buildAuthorizeContext: () => toWebRequest(request)
385
+ });
386
+ if (authError) {
387
+ if (authError.headers) for (const [key, value] of Object.entries(authError.headers)) reply.header(key, value);
388
+ if (authError.error) return reply.status(authError.status).send({ error: authError.error });
389
+ }
390
+ const params = await this.getParams(route, request);
391
+ if (params.bodyParseError) return reply.status(400).send({
392
+ error: "Invalid request body",
393
+ issues: [{
394
+ field: "body",
395
+ message: params.bodyParseError.message
396
+ }]
397
+ });
398
+ if (params.queryParams) try {
399
+ params.queryParams = await this.parseQueryParams(route, params.queryParams);
400
+ } catch (error) {
401
+ this.mastra.getLogger()?.error("Error parsing query params", { error: error instanceof Error ? {
402
+ message: error.message,
403
+ stack: error.stack
404
+ } : error });
405
+ if (isZodError(error)) {
406
+ const { status, body } = this.resolveValidationError(route, error, "query");
407
+ return reply.status(status).send(body);
408
+ }
409
+ return reply.status(400).send({
410
+ error: "Invalid query parameters",
411
+ issues: [{
412
+ field: "unknown",
413
+ message: error instanceof Error ? error.message : "Unknown error"
414
+ }]
415
+ });
416
+ }
417
+ if (params.body) try {
418
+ params.body = await this.parseBody(route, params.body);
419
+ } catch (error) {
420
+ this.mastra.getLogger()?.error("Error parsing body", { error: error instanceof Error ? {
421
+ message: error.message,
422
+ stack: error.stack
423
+ } : error });
424
+ if (isZodError(error)) {
425
+ const { status, body } = this.resolveValidationError(route, error, "body");
426
+ return reply.status(status).send(body);
427
+ }
428
+ return reply.status(400).send({
429
+ error: "Invalid request body",
430
+ issues: [{
431
+ field: "unknown",
432
+ message: error instanceof Error ? error.message : "Unknown error"
433
+ }]
434
+ });
435
+ }
436
+ if (params.urlParams) try {
437
+ params.urlParams = await this.parsePathParams(route, params.urlParams);
438
+ } catch (error) {
439
+ this.mastra.getLogger()?.error("Error parsing path params", { error: error instanceof Error ? {
440
+ message: error.message,
441
+ stack: error.stack
442
+ } : error });
443
+ if (isZodError(error)) {
444
+ const { status, body } = this.resolveValidationError(route, error, "path");
445
+ return reply.status(status).send(body);
446
+ }
447
+ return reply.status(400).send({
448
+ error: "Invalid path parameters",
449
+ issues: [{
450
+ field: "unknown",
451
+ message: error instanceof Error ? error.message : "Unknown error"
452
+ }]
453
+ });
454
+ }
455
+ const handlerParams = {
456
+ ...params.urlParams,
457
+ ...params.queryParams,
458
+ ...typeof params.body === "object" ? params.body : {},
459
+ requestContext: request.requestContext,
460
+ mastra: this.mastra,
461
+ registeredTools: request.registeredTools,
462
+ taskStore: request.taskStore,
463
+ abortSignal: request.abortSignal,
464
+ routePrefix: prefix,
465
+ request: toWebRequest(request)
466
+ };
467
+ const requestContext = request.requestContext;
468
+ if (this.mastra.getStudio?.()?.auth || this.mastra.getServer()?.auth) {
469
+ const hasPermission = await loadHasPermission();
470
+ if (hasPermission) {
471
+ const userPermissions = requestContext.get("mastra__userPermissions");
472
+ const permissionError = this.checkRoutePermission(route, userPermissions, hasPermission, requestContext);
473
+ if (permissionError) return reply.status(permissionError.status).send({
474
+ error: permissionError.error,
475
+ message: permissionError.message
476
+ });
477
+ }
478
+ }
479
+ const fgaError = await checkRouteFGA(this.mastra, route, requestContext, {
480
+ ...params.urlParams,
481
+ ...params.queryParams,
482
+ ...typeof params.body === "object" ? params.body : {}
483
+ });
484
+ if (fgaError) return reply.status(fgaError.status).send({
485
+ error: fgaError.error,
486
+ message: fgaError.message
487
+ });
488
+ try {
489
+ const result = await route.handler(handlerParams);
490
+ await this.sendResponse(route, reply, result, request, prefix);
491
+ } catch (error) {
492
+ const httpStatus = error && typeof error === "object" && "status" in error ? error.status : void 0;
493
+ if (!(typeof httpStatus === "number" && httpStatus >= 400 && httpStatus < 500)) this.mastra.getLogger()?.error("Error calling handler", {
494
+ error: error instanceof Error ? {
495
+ message: error.message,
496
+ stack: error.stack
497
+ } : error,
498
+ path: route.path,
499
+ method: route.method
500
+ });
501
+ let status = 500;
502
+ if (error && typeof error === "object") {
503
+ if ("status" in error) status = error.status;
504
+ else if ("details" in error && error.details && typeof error.details === "object" && "status" in error.details) status = error.details.status;
505
+ }
506
+ await reply.status(status).send({ error: error instanceof Error ? error.message : "Unknown error" });
507
+ }
508
+ };
509
+ const shouldApplyBodyLimit = this.bodyLimitOptions && [
510
+ "POST",
511
+ "PUT",
512
+ "PATCH"
513
+ ].includes(route.method.toUpperCase());
514
+ const maxSize = route.maxBodySize ?? this.bodyLimitOptions?.maxSize;
515
+ const config = shouldApplyBodyLimit && maxSize ? { bodyLimit: maxSize } : void 0;
516
+ if (route.method.toUpperCase() === "ALL") for (const method of [
517
+ "GET",
518
+ "POST",
519
+ "PUT",
520
+ "DELETE",
521
+ "PATCH"
522
+ ]) try {
523
+ app.route({
524
+ method,
525
+ url: fastifyPath,
526
+ handler,
527
+ config
528
+ });
529
+ } catch (err) {
530
+ if (err instanceof Error && err.message.includes("already declared")) continue;
531
+ throw err;
532
+ }
533
+ else app.route({
534
+ method: route.method,
535
+ url: fastifyPath,
536
+ handler,
537
+ config
538
+ });
539
+ }
540
+ async registerCustomApiRoutes() {
541
+ if (!await this.buildCustomRouteHandler()) return;
542
+ const routes = this.customApiRoutes ?? this.mastra.getServer()?.apiRoutes ?? [];
543
+ for (const route of routes) {
544
+ const serverRoute = {
545
+ method: route.method,
546
+ path: route.path,
547
+ responseType: "json",
548
+ handler: async () => {},
549
+ requiresAuth: route.requiresAuth,
550
+ requiresPermission: route.requiresPermission,
551
+ fga: route.fga
552
+ };
553
+ const fastifyHandler = async (request, reply) => {
554
+ const authError = await this.checkRouteAuth(serverRoute, {
555
+ path: String(request.url.split("?")[0] || "/"),
556
+ method: String(request.method || "GET"),
557
+ getHeader: (name) => request.headers[name.toLowerCase()],
558
+ getQuery: (name) => request.query[name],
559
+ requestContext: request.requestContext,
560
+ request: toWebRequest(request),
561
+ buildAuthorizeContext: () => toWebRequest(request)
562
+ });
563
+ if (authError) {
564
+ if (authError.headers) for (const [key, value] of Object.entries(authError.headers)) reply.header(key, value);
565
+ if (authError.error) return reply.status(authError.status).send({ error: authError.error });
566
+ }
567
+ const requestContext = request.requestContext;
568
+ if (this.mastra.getStudio?.()?.auth || this.mastra.getServer()?.auth) {
569
+ let hasPermission;
570
+ try {
571
+ ({hasPermission} = await import("@mastra/core/auth/ee"));
572
+ } catch {
573
+ console.error("[@mastra/fastify] Auth features require @mastra/core >= 1.6.0. Please upgrade: npm install @mastra/core@latest");
574
+ }
575
+ if (hasPermission) {
576
+ const userPermissions = requestContext.get("mastra__userPermissions");
577
+ const permissionError = this.checkRoutePermission(serverRoute, userPermissions, hasPermission, requestContext);
578
+ if (permissionError) return reply.status(permissionError.status).send({
579
+ error: permissionError.error,
580
+ message: permissionError.message
581
+ });
582
+ }
583
+ }
584
+ const fgaError = await checkRouteFGA(this.mastra, serverRoute, requestContext, {
585
+ ...request.params,
586
+ ...request.query,
587
+ ...typeof request.body === "object" && request.body !== null ? request.body : {}
588
+ });
589
+ if (fgaError) return reply.status(fgaError.status).send({
590
+ error: fgaError.error,
591
+ message: fgaError.message
592
+ });
593
+ const response = await this.handleCustomRouteRequest(`http://${request.headers.host}${request.url}`, request.method, request.headers, request.body, request.requestContext, request.abortSignal);
594
+ if (!response) {
595
+ reply.status(404).send({ error: "Not Found" });
596
+ return;
597
+ }
598
+ const existingHeaders = reply.getHeaders();
599
+ for (const [key, value] of Object.entries(existingHeaders)) {
600
+ if (value === void 0) continue;
601
+ const lowerKey = key.toLowerCase();
602
+ if (lowerKey === "content-length" || lowerKey === "transfer-encoding") continue;
603
+ const isSetCookie = lowerKey === "set-cookie";
604
+ if (!isSetCookie && response.headers.has(key)) continue;
605
+ if (Array.isArray(value)) for (const item of value) response.headers.append(key, String(item));
606
+ else if (isSetCookie) response.headers.append(key, String(value));
607
+ else response.headers.set(key, String(value));
608
+ }
609
+ reply.hijack();
610
+ await this.writeCustomRouteResponse(response, reply.raw, request.abortSignal);
611
+ };
612
+ if (route.method === "ALL") for (const method of [
613
+ "GET",
614
+ "POST",
615
+ "PUT",
616
+ "DELETE",
617
+ "PATCH"
618
+ ]) this.app.route({
619
+ method,
620
+ url: route.path,
621
+ handler: fastifyHandler
622
+ });
623
+ else this.app.route({
624
+ method: route.method,
625
+ url: route.path,
626
+ handler: fastifyHandler
627
+ });
628
+ }
629
+ }
630
+ registerContextMiddleware() {
631
+ this.app.removeContentTypeParser("application/json");
632
+ this.app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
633
+ try {
634
+ if (!body || typeof body === "string" && body.trim() === "") {
635
+ done(null, void 0);
636
+ return;
637
+ }
638
+ done(null, JSON.parse(body));
639
+ } catch (err) {
640
+ done(err, void 0);
641
+ }
642
+ });
643
+ this.app.addContentTypeParser("multipart/form-data", (_request, _payload, done) => {
644
+ done(null, void 0);
645
+ });
646
+ this.app.addHook("preHandler", this.createContextMiddleware());
647
+ }
648
+ registerAuthMiddleware() {}
649
+ registerHttpLoggingMiddleware() {
650
+ if (!this.httpLoggingConfig?.enabled) return;
651
+ this.app.addHook("onRequest", async (request, reply) => {
652
+ const urlPath = request.url.split("?")[0];
653
+ if (!this.shouldLogRequest(urlPath)) return;
654
+ const start = Date.now();
655
+ const method = request.method;
656
+ const path = urlPath;
657
+ reply.raw.once("finish", () => {
658
+ const duration = Date.now() - start;
659
+ const status = reply.statusCode;
660
+ const level = this.httpLoggingConfig?.level || "info";
661
+ const logData = {
662
+ method,
663
+ path,
664
+ status,
665
+ duration: `${duration}ms`
666
+ };
667
+ if (this.httpLoggingConfig?.includeQueryParams) logData.query = request.query;
668
+ if (this.httpLoggingConfig?.includeHeaders) {
669
+ const headers = { ...request.headers };
670
+ (this.httpLoggingConfig.redactHeaders || []).forEach((h) => {
671
+ const key = h.toLowerCase();
672
+ if (headers[key] !== void 0) headers[key] = "[REDACTED]";
673
+ });
674
+ logData.headers = headers;
675
+ }
676
+ this.logger[level](`${method} ${path} ${status} ${duration}ms`, logData);
677
+ });
678
+ });
679
+ }
793
680
  };
794
-
681
+ //#endregion
795
682
  export { MastraServer, createAuthMiddleware };
796
- //# sourceMappingURL=index.js.map
683
+
797
684
  //# sourceMappingURL=index.js.map