@dxheroes/local-mcp-backend 0.9.2 → 0.10.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.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +27 -0
  3. package/dist/__tests__/integration/mcp-proxy-auth-http.test.js +246 -0
  4. package/dist/__tests__/integration/mcp-proxy-auth-http.test.js.map +1 -0
  5. package/dist/__tests__/integration/oauth-authorize-callback.test.js +122 -0
  6. package/dist/__tests__/integration/oauth-authorize-callback.test.js.map +1 -0
  7. package/dist/__tests__/integration/proxy-auth.test.js +121 -111
  8. package/dist/__tests__/integration/proxy-auth.test.js.map +1 -1
  9. package/dist/__tests__/unit/auth.guard.test.js +23 -2
  10. package/dist/__tests__/unit/auth.guard.test.js.map +1 -1
  11. package/dist/common/filters/all-exceptions.filter.js +6 -0
  12. package/dist/common/filters/all-exceptions.filter.js.map +1 -1
  13. package/dist/main.js +37 -0
  14. package/dist/main.js.map +1 -1
  15. package/dist/modules/auth/auth.config.js +5 -2
  16. package/dist/modules/auth/auth.config.js.map +1 -1
  17. package/dist/modules/auth/auth.module.js +5 -2
  18. package/dist/modules/auth/auth.module.js.map +1 -1
  19. package/dist/modules/auth/auth.service.js +2 -2
  20. package/dist/modules/auth/auth.service.js.map +1 -1
  21. package/dist/modules/auth/mcp-oauth.guard.js +70 -0
  22. package/dist/modules/auth/mcp-oauth.guard.js.map +1 -0
  23. package/dist/modules/auth/mcp-oauth.utils.js +75 -0
  24. package/dist/modules/auth/mcp-oauth.utils.js.map +1 -0
  25. package/dist/modules/mcp/mcp.service.js +48 -8
  26. package/dist/modules/mcp/mcp.service.js.map +1 -1
  27. package/dist/modules/oauth/oauth.controller.js +78 -1
  28. package/dist/modules/oauth/oauth.controller.js.map +1 -1
  29. package/dist/modules/oauth/oauth.service.js +197 -1
  30. package/dist/modules/oauth/oauth.service.js.map +1 -1
  31. package/dist/modules/proxy/proxy.controller.js +152 -27
  32. package/dist/modules/proxy/proxy.controller.js.map +1 -1
  33. package/dist/modules/proxy/proxy.service.js +28 -4
  34. package/dist/modules/proxy/proxy.service.js.map +1 -1
  35. package/docker-entrypoint.sh +15 -2
  36. package/package.json +7 -7
  37. package/src/__tests__/integration/mcp-proxy-auth-http.test.ts +281 -0
  38. package/src/__tests__/integration/oauth-authorize-callback.test.ts +155 -0
  39. package/src/__tests__/integration/proxy-auth.test.ts +119 -168
  40. package/src/__tests__/unit/auth.guard.test.ts +12 -2
  41. package/src/common/filters/all-exceptions.filter.ts +11 -0
  42. package/src/main.ts +32 -1
  43. package/src/modules/auth/auth.config.ts +4 -1
  44. package/src/modules/auth/auth.module.ts +3 -2
  45. package/src/modules/auth/auth.service.ts +2 -2
  46. package/src/modules/auth/mcp-oauth.guard.ts +75 -0
  47. package/src/modules/auth/mcp-oauth.utils.ts +80 -0
  48. package/src/modules/mcp/mcp.service.ts +54 -12
  49. package/src/modules/oauth/oauth.controller.ts +84 -1
  50. package/src/modules/oauth/oauth.service.ts +218 -1
  51. package/src/modules/proxy/proxy.controller.ts +120 -25
  52. package/src/modules/proxy/proxy.service.ts +26 -4
  53. package/vitest.config.ts +2 -1
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/main.ts"],"sourcesContent":["/**\n * NestJS Application Bootstrap\n *\n * Entry point for the Local MCP Gateway backend.\n * Better Auth handles /api/auth/* and /.well-known/* routes.\n */\n\nimport { Logger, ValidationPipe } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { NestFactory } from '@nestjs/core';\nimport { toNodeHandler } from 'better-auth/node';\nimport compression from 'compression';\nimport helmet from 'helmet';\nimport { AppModule } from './app.module.js';\nimport { AllExceptionsFilter } from './common/filters/all-exceptions.filter.js';\nimport { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';\nimport { AuthService } from './modules/auth/auth.service.js';\n\nasync function bootstrap() {\n // Determine log levels from environment\n const logLevel = process.env.LOG_LEVEL || 'log';\n const logLevels: ('error' | 'warn' | 'log' | 'debug' | 'verbose')[] = ['error'];\n if (['warn', 'log', 'debug', 'verbose'].includes(logLevel)) logLevels.push('warn');\n if (['log', 'debug', 'verbose'].includes(logLevel)) logLevels.push('log');\n if (['debug', 'verbose'].includes(logLevel)) logLevels.push('debug');\n if (logLevel === 'verbose') logLevels.push('verbose');\n\n const logger = new Logger('Bootstrap');\n const app = await NestFactory.create(AppModule, {\n logger: logLevels,\n });\n\n const configService = app.get(ConfigService);\n\n // Security\n app.use(helmet());\n app.use(compression());\n\n // CORS\n const corsOrigins = configService.get<string>('CORS_ORIGINS')?.split(',') || [\n 'http://localhost:5173',\n 'http://localhost:3000',\n ];\n app.enableCors({\n origin: corsOrigins,\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n });\n\n // Global prefix\n app.setGlobalPrefix('api');\n\n // Global pipes\n app.useGlobalPipes(\n new ValidationPipe({\n whitelist: true,\n forbidNonWhitelisted: true,\n transform: true,\n transformOptions: { enableImplicitConversion: true },\n })\n );\n\n // Global filters\n app.useGlobalFilters(new AllExceptionsFilter());\n\n // Global interceptors\n app.useGlobalInterceptors(new LoggingInterceptor());\n\n // Mount Better Auth handler on Express BEFORE app.init() so it registers\n // ahead of NestJS's catch-all 404 handler. We use a lazy wrapper because\n // AuthService.onModuleInit() hasn't run yet — auth initializes during app.init().\n const authService = app.get(AuthService);\n const expressApp = app.getHttpAdapter().getInstance();\n\n const lazyAuthHandler = (req: any, res: any, next: any) => {\n const auth = authService.getAuth();\n if (!auth) return next();\n toNodeHandler(auth)(req, res);\n };\n\n expressApp.all('/api/auth/*splat', lazyAuthHandler);\n expressApp.all('/.well-known/*splat', lazyAuthHandler);\n\n logger.log('Better Auth routes registered (lazy) on /api/auth/* and /.well-known/*');\n\n const port = configService.get<number>('PORT') || 3001;\n await app.listen(port);\n\n logger.log(`Application is running on: http://localhost:${port}`);\n logger.log(`API available at: http://localhost:${port}/api`);\n logger.log('Auth: always enabled (email+password baseline)');\n}\n\nbootstrap();\n"],"names":["Logger","ValidationPipe","ConfigService","NestFactory","toNodeHandler","compression","helmet","AppModule","AllExceptionsFilter","LoggingInterceptor","AuthService","bootstrap","logLevel","process","env","LOG_LEVEL","logLevels","includes","push","logger","app","create","configService","get","use","corsOrigins","split","enableCors","origin","credentials","methods","setGlobalPrefix","useGlobalPipes","whitelist","forbidNonWhitelisted","transform","transformOptions","enableImplicitConversion","useGlobalFilters","useGlobalInterceptors","authService","expressApp","getHttpAdapter","getInstance","lazyAuthHandler","req","res","next","auth","getAuth","all","log","port","listen"],"mappings":"AAAA;;;;;CAKC,GAED,SAASA,MAAM,EAAEC,cAAc,QAAQ,iBAAiB;AACxD,SAASC,aAAa,QAAQ,iBAAiB;AAC/C,SAASC,WAAW,QAAQ,eAAe;AAC3C,SAASC,aAAa,QAAQ,mBAAmB;AACjD,OAAOC,iBAAiB,cAAc;AACtC,OAAOC,YAAY,SAAS;AAC5B,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,mBAAmB,QAAQ,4CAA4C;AAChF,SAASC,kBAAkB,QAAQ,+CAA+C;AAClF,SAASC,WAAW,QAAQ,iCAAiC;AAE7D,eAAeC;IACb,wCAAwC;IACxC,MAAMC,WAAWC,QAAQC,GAAG,CAACC,SAAS,IAAI;IAC1C,MAAMC,YAAgE;QAAC;KAAQ;IAC/E,IAAI;QAAC;QAAQ;QAAO;QAAS;KAAU,CAACC,QAAQ,CAACL,WAAWI,UAAUE,IAAI,CAAC;IAC3E,IAAI;QAAC;QAAO;QAAS;KAAU,CAACD,QAAQ,CAACL,WAAWI,UAAUE,IAAI,CAAC;IACnE,IAAI;QAAC;QAAS;KAAU,CAACD,QAAQ,CAACL,WAAWI,UAAUE,IAAI,CAAC;IAC5D,IAAIN,aAAa,WAAWI,UAAUE,IAAI,CAAC;IAE3C,MAAMC,SAAS,IAAInB,OAAO;IAC1B,MAAMoB,MAAM,MAAMjB,YAAYkB,MAAM,CAACd,WAAW;QAC9CY,QAAQH;IACV;IAEA,MAAMM,gBAAgBF,IAAIG,GAAG,CAACrB;IAE9B,WAAW;IACXkB,IAAII,GAAG,CAAClB;IACRc,IAAII,GAAG,CAACnB;IAER,OAAO;IACP,MAAMoB,cAAcH,cAAcC,GAAG,CAAS,iBAAiBG,MAAM,QAAQ;QAC3E;QACA;KACD;IACDN,IAAIO,UAAU,CAAC;QACbC,QAAQH;QACRI,aAAa;QACbC,SAAS;YAAC;YAAO;YAAQ;YAAO;YAAU;YAAS;SAAU;IAC/D;IAEA,gBAAgB;IAChBV,IAAIW,eAAe,CAAC;IAEpB,eAAe;IACfX,IAAIY,cAAc,CAChB,IAAI/B,eAAe;QACjBgC,WAAW;QACXC,sBAAsB;QACtBC,WAAW;QACXC,kBAAkB;YAAEC,0BAA0B;QAAK;IACrD;IAGF,iBAAiB;IACjBjB,IAAIkB,gBAAgB,CAAC,IAAI9B;IAEzB,sBAAsB;IACtBY,IAAImB,qBAAqB,CAAC,IAAI9B;IAE9B,yEAAyE;IACzE,yEAAyE;IACzE,kFAAkF;IAClF,MAAM+B,cAAcpB,IAAIG,GAAG,CAACb;IAC5B,MAAM+B,aAAarB,IAAIsB,cAAc,GAAGC,WAAW;IAEnD,MAAMC,kBAAkB,CAACC,KAAUC,KAAUC;QAC3C,MAAMC,OAAOR,YAAYS,OAAO;QAChC,IAAI,CAACD,MAAM,OAAOD;QAClB3C,cAAc4C,MAAMH,KAAKC;IAC3B;IAEAL,WAAWS,GAAG,CAAC,oBAAoBN;IACnCH,WAAWS,GAAG,CAAC,uBAAuBN;IAEtCzB,OAAOgC,GAAG,CAAC;IAEX,MAAMC,OAAO9B,cAAcC,GAAG,CAAS,WAAW;IAClD,MAAMH,IAAIiC,MAAM,CAACD;IAEjBjC,OAAOgC,GAAG,CAAC,CAAC,4CAA4C,EAAEC,MAAM;IAChEjC,OAAOgC,GAAG,CAAC,CAAC,mCAAmC,EAAEC,KAAK,IAAI,CAAC;IAC3DjC,OAAOgC,GAAG,CAAC;AACb;AAEAxC"}
1
+ {"version":3,"sources":["../src/main.ts"],"sourcesContent":["/**\n * NestJS Application Bootstrap\n *\n * Entry point for the Local MCP Gateway backend.\n * Better Auth handles /api/auth/* and /.well-known/* routes.\n */\n\nimport { Logger, ValidationPipe } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { NestFactory } from '@nestjs/core';\nimport { toNodeHandler } from 'better-auth/node';\nimport compression from 'compression';\nimport type { NextFunction, Request, Response } from 'express';\nimport helmet from 'helmet';\nimport { AppModule } from './app.module.js';\nimport { AllExceptionsFilter } from './common/filters/all-exceptions.filter.js';\nimport { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';\nimport { AuthService } from './modules/auth/auth.service.js';\nimport {\n createMcpProtectedResourceMetadata,\n resolvePublicAuthBaseUrl,\n resolvePublicBackendOrigin,\n} from './modules/auth/mcp-oauth.utils.js';\n\nasync function bootstrap() {\n // Determine log levels from environment\n const logLevel = process.env.LOG_LEVEL || 'log';\n const logLevels: ('error' | 'warn' | 'log' | 'debug' | 'verbose')[] = ['error'];\n if (['warn', 'log', 'debug', 'verbose'].includes(logLevel)) logLevels.push('warn');\n if (['log', 'debug', 'verbose'].includes(logLevel)) logLevels.push('log');\n if (['debug', 'verbose'].includes(logLevel)) logLevels.push('debug');\n if (logLevel === 'verbose') logLevels.push('verbose');\n\n const logger = new Logger('Bootstrap');\n const app = await NestFactory.create(AppModule, {\n logger: logLevels,\n });\n\n const configService = app.get(ConfigService);\n\n // Security\n app.use(helmet());\n app.use(compression());\n\n // CORS\n const corsOrigins = configService.get<string>('CORS_ORIGINS')?.split(',') || [\n 'http://localhost:5173',\n 'http://localhost:3000',\n ];\n app.enableCors({\n origin: corsOrigins,\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n });\n\n // Global prefix\n app.setGlobalPrefix('api');\n\n // Global pipes\n app.useGlobalPipes(\n new ValidationPipe({\n whitelist: true,\n forbidNonWhitelisted: true,\n transform: true,\n transformOptions: { enableImplicitConversion: true },\n })\n );\n\n // Global filters\n app.useGlobalFilters(new AllExceptionsFilter());\n\n // Global interceptors\n app.useGlobalInterceptors(new LoggingInterceptor());\n\n // Mount Better Auth handler on Express BEFORE app.init() so it registers\n // ahead of NestJS's catch-all 404 handler. We use a lazy wrapper because\n // AuthService.onModuleInit() hasn't run yet — auth initializes during app.init().\n const authService = app.get(AuthService);\n const expressApp = app.getHttpAdapter().getInstance();\n\n const lazyAuthHandler = (req: Request, res: Response, next: NextFunction) => {\n const auth = authService.getAuth();\n if (!auth) return next();\n toNodeHandler(auth)(req, res);\n };\n\n expressApp.get('/.well-known/oauth-protected-resource', (_req: Request, res: Response) => {\n res.json(createMcpProtectedResourceMetadata(configService));\n });\n\n // RFC 8414 – OAuth 2.0 Authorization Server Metadata\n // MCP clients fetch this to discover the correct DCR endpoint (/api/auth/mcp/register)\n // instead of falling back to the root /register which returns 404.\n expressApp.get('/.well-known/oauth-authorization-server', (_req: Request, res: Response) => {\n const backendOrigin = resolvePublicBackendOrigin(configService);\n const authBaseUrl = resolvePublicAuthBaseUrl(configService);\n\n res.json({\n issuer: backendOrigin,\n authorization_endpoint: `${authBaseUrl}/mcp/authorize`,\n token_endpoint: `${authBaseUrl}/mcp/token`,\n registration_endpoint: `${authBaseUrl}/mcp/register`,\n jwks_uri: `${authBaseUrl}/mcp/jwks`,\n response_types_supported: ['code'],\n grant_types_supported: ['authorization_code', 'refresh_token'],\n token_endpoint_auth_methods_supported: ['none'],\n code_challenge_methods_supported: ['S256'],\n scopes_supported: ['openid', 'profile', 'email', 'offline_access'],\n });\n });\n\n expressApp.all('/api/auth/*splat', lazyAuthHandler);\n expressApp.all('/.well-known/*splat', lazyAuthHandler);\n\n logger.log('Better Auth routes registered (lazy) on /api/auth/* and /.well-known/*');\n\n const port = configService.get<number>('PORT') || 3001;\n await app.listen(port);\n\n logger.log(`Application is running on: http://localhost:${port}`);\n logger.log(`API available at: http://localhost:${port}/api`);\n logger.log('Auth: always enabled (email+password baseline)');\n}\n\nbootstrap();\n"],"names":["Logger","ValidationPipe","ConfigService","NestFactory","toNodeHandler","compression","helmet","AppModule","AllExceptionsFilter","LoggingInterceptor","AuthService","createMcpProtectedResourceMetadata","resolvePublicAuthBaseUrl","resolvePublicBackendOrigin","bootstrap","logLevel","process","env","LOG_LEVEL","logLevels","includes","push","logger","app","create","configService","get","use","corsOrigins","split","enableCors","origin","credentials","methods","setGlobalPrefix","useGlobalPipes","whitelist","forbidNonWhitelisted","transform","transformOptions","enableImplicitConversion","useGlobalFilters","useGlobalInterceptors","authService","expressApp","getHttpAdapter","getInstance","lazyAuthHandler","req","res","next","auth","getAuth","_req","json","backendOrigin","authBaseUrl","issuer","authorization_endpoint","token_endpoint","registration_endpoint","jwks_uri","response_types_supported","grant_types_supported","token_endpoint_auth_methods_supported","code_challenge_methods_supported","scopes_supported","all","log","port","listen"],"mappings":"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"}
@@ -4,10 +4,12 @@
4
4
  * Configures Better Auth with Prisma adapter, email+password (always),
5
5
  * optional Google OAuth, Organization plugin, and MCP OAuth plugin.
6
6
  * Auto-creates a default organization on user signup.
7
- */ import { betterAuth } from "better-auth";
7
+ */ import { ConfigService } from "@nestjs/config";
8
+ import { betterAuth } from "better-auth";
8
9
  import { prismaAdapter } from "better-auth/adapters/prisma";
9
10
  import { mcp } from "better-auth/plugins";
10
11
  import { organization } from "better-auth/plugins/organization";
12
+ import { resolveMcpLoginPageUrl } from "./mcp-oauth.utils.js";
11
13
  /**
12
14
  * Generate a URL-safe slug from a name.
13
15
  */ function toSlug(name) {
@@ -15,6 +17,7 @@ import { organization } from "better-auth/plugins/organization";
15
17
  }
16
18
  export function createAuth(prisma) {
17
19
  const hasGoogle = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
20
+ const configService = new ConfigService();
18
21
  const auth = betterAuth({
19
22
  basePath: '/api/auth',
20
23
  baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',
@@ -87,7 +90,7 @@ export function createAuth(prisma) {
87
90
  plugins: [
88
91
  organization(),
89
92
  mcp({
90
- loginPage: '/sign-in'
93
+ loginPage: resolveMcpLoginPageUrl(configService)
91
94
  })
92
95
  ]
93
96
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/auth/auth.config.ts"],"sourcesContent":["/**\n * Better Auth Configuration\n *\n * Configures Better Auth with Prisma adapter, email+password (always),\n * optional Google OAuth, Organization plugin, and MCP OAuth plugin.\n * Auto-creates a default organization on user signup.\n */\n\nimport type { PrismaClient } from '@dxheroes/local-mcp-database/generated/prisma';\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { mcp } from 'better-auth/plugins';\nimport { organization } from 'better-auth/plugins/organization';\n\n/**\n * Auth wrapper — simplified interface to avoid exporting Better Auth's deep generic types.\n */\nexport interface AuthInstance {\n handler: (req: Request) => Promise<Response>;\n api: {\n getSession: (opts: { headers: Headers }) => Promise<{\n user: { id: string; name: string; email: string; image: string | null };\n session: { id: string; userId: string; [key: string]: unknown };\n } | null>;\n };\n}\n\n/**\n * Generate a URL-safe slug from a name.\n */\nfunction toSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nexport function createAuth(prisma: PrismaClient): AuthInstance {\n const hasGoogle = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;\n\n const auth = betterAuth({\n basePath: '/api/auth',\n baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',\n secret: process.env.BETTER_AUTH_SECRET,\n trustedOrigins: (process.env.CORS_ORIGINS?.split(',') || [\n 'http://localhost:5173',\n 'http://localhost:3000',\n ]),\n database: prismaAdapter(prisma, { provider: 'postgresql' }),\n emailAndPassword: { enabled: true },\n ...(hasGoogle && {\n socialProviders: {\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID ?? '',\n clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',\n },\n },\n }),\n session: {\n cookieCache: {\n enabled: true,\n maxAge: 5 * 60, // 5 minutes\n },\n },\n databaseHooks: {\n user: {\n create: {\n after: async (user) => {\n // Auto-create a default organization for new users\n const orgName = `${user.name}'s workspace`;\n const baseSlug = `${toSlug(user.name)}-workspace`;\n\n // Ensure unique slug\n let slug = baseSlug;\n let suffix = 0;\n while (true) {\n const existing = await prisma.organization.findUnique({ where: { slug } });\n if (!existing) break;\n suffix++;\n slug = `${baseSlug}-${suffix}`;\n }\n\n const orgId = crypto.randomUUID();\n const memberId = crypto.randomUUID();\n\n await prisma.organization.create({\n data: {\n id: orgId,\n name: orgName,\n slug,\n },\n });\n\n await prisma.member.create({\n data: {\n id: memberId,\n organizationId: orgId,\n userId: user.id,\n role: 'owner',\n },\n });\n },\n },\n },\n },\n plugins: [\n organization(),\n mcp({\n loginPage: '/sign-in',\n }),\n ],\n });\n return auth as unknown as AuthInstance;\n}\n"],"names":["betterAuth","prismaAdapter","mcp","organization","toSlug","name","toLowerCase","replace","createAuth","prisma","hasGoogle","process","env","GOOGLE_CLIENT_ID","GOOGLE_CLIENT_SECRET","auth","basePath","baseURL","BETTER_AUTH_URL","secret","BETTER_AUTH_SECRET","trustedOrigins","CORS_ORIGINS","split","database","provider","emailAndPassword","enabled","socialProviders","google","clientId","clientSecret","session","cookieCache","maxAge","databaseHooks","user","create","after","orgName","baseSlug","slug","suffix","existing","findUnique","where","orgId","crypto","randomUUID","memberId","data","id","member","organizationId","userId","role","plugins","loginPage"],"mappings":"AAAA;;;;;;CAMC,GAGD,SAASA,UAAU,QAAQ,cAAc;AACzC,SAASC,aAAa,QAAQ,8BAA8B;AAC5D,SAASC,GAAG,QAAQ,sBAAsB;AAC1C,SAASC,YAAY,QAAQ,mCAAmC;AAehE;;CAEC,GACD,SAASC,OAAOC,IAAY;IAC1B,OAAOA,KACJC,WAAW,GACXC,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,UAAU;AACvB;AAEA,OAAO,SAASC,WAAWC,MAAoB;IAC7C,MAAMC,YAAY,CAAC,CAACC,QAAQC,GAAG,CAACC,gBAAgB,IAAI,CAAC,CAACF,QAAQC,GAAG,CAACE,oBAAoB;IAEtF,MAAMC,OAAOf,WAAW;QACtBgB,UAAU;QACVC,SAASN,QAAQC,GAAG,CAACM,eAAe,IAAI;QACxCC,QAAQR,QAAQC,GAAG,CAACQ,kBAAkB;QACtCC,gBAAiBV,QAAQC,GAAG,CAACU,YAAY,EAAEC,MAAM,QAAQ;YACvD;YACA;SACD;QACDC,UAAUvB,cAAcQ,QAAQ;YAAEgB,UAAU;QAAa;QACzDC,kBAAkB;YAAEC,SAAS;QAAK;QAClC,GAAIjB,aAAa;YACfkB,iBAAiB;gBACfC,QAAQ;oBACNC,UAAUnB,QAAQC,GAAG,CAACC,gBAAgB,IAAI;oBAC1CkB,cAAcpB,QAAQC,GAAG,CAACE,oBAAoB,IAAI;gBACpD;YACF;QACF,CAAC;QACDkB,SAAS;YACPC,aAAa;gBACXN,SAAS;gBACTO,QAAQ,IAAI;YACd;QACF;QACAC,eAAe;YACbC,MAAM;gBACJC,QAAQ;oBACNC,OAAO,OAAOF;wBACZ,mDAAmD;wBACnD,MAAMG,UAAU,GAAGH,KAAK/B,IAAI,CAAC,YAAY,CAAC;wBAC1C,MAAMmC,WAAW,GAAGpC,OAAOgC,KAAK/B,IAAI,EAAE,UAAU,CAAC;wBAEjD,qBAAqB;wBACrB,IAAIoC,OAAOD;wBACX,IAAIE,SAAS;wBACb,MAAO,KAAM;4BACX,MAAMC,WAAW,MAAMlC,OAAON,YAAY,CAACyC,UAAU,CAAC;gCAAEC,OAAO;oCAAEJ;gCAAK;4BAAE;4BACxE,IAAI,CAACE,UAAU;4BACfD;4BACAD,OAAO,GAAGD,SAAS,CAAC,EAAEE,QAAQ;wBAChC;wBAEA,MAAMI,QAAQC,OAAOC,UAAU;wBAC/B,MAAMC,WAAWF,OAAOC,UAAU;wBAElC,MAAMvC,OAAON,YAAY,CAACkC,MAAM,CAAC;4BAC/Ba,MAAM;gCACJC,IAAIL;gCACJzC,MAAMkC;gCACNE;4BACF;wBACF;wBAEA,MAAMhC,OAAO2C,MAAM,CAACf,MAAM,CAAC;4BACzBa,MAAM;gCACJC,IAAIF;gCACJI,gBAAgBP;gCAChBQ,QAAQlB,KAAKe,EAAE;gCACfI,MAAM;4BACR;wBACF;oBACF;gBACF;YACF;QACF;QACAC,SAAS;YACPrD;YACAD,IAAI;gBACFuD,WAAW;YACb;SACD;IACH;IACA,OAAO1C;AACT"}
1
+ {"version":3,"sources":["../../../src/modules/auth/auth.config.ts"],"sourcesContent":["/**\n * Better Auth Configuration\n *\n * Configures Better Auth with Prisma adapter, email+password (always),\n * optional Google OAuth, Organization plugin, and MCP OAuth plugin.\n * Auto-creates a default organization on user signup.\n */\n\nimport type { PrismaClient } from '@dxheroes/local-mcp-database/generated/prisma';\nimport { ConfigService } from '@nestjs/config';\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { mcp } from 'better-auth/plugins';\nimport { organization } from 'better-auth/plugins/organization';\nimport { resolveMcpLoginPageUrl } from './mcp-oauth.utils.js';\n\n/**\n * Auth wrapper — simplified interface to avoid exporting Better Auth's deep generic types.\n */\nexport interface AuthInstance {\n handler: (req: Request) => Promise<Response>;\n api: {\n getSession: (opts: { headers: Headers }) => Promise<{\n user: { id: string; name: string; email: string; image: string | null };\n session: { id: string; userId: string; [key: string]: unknown };\n } | null>;\n };\n}\n\n/**\n * Generate a URL-safe slug from a name.\n */\nfunction toSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nexport function createAuth(prisma: PrismaClient): AuthInstance {\n const hasGoogle = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;\n const configService = new ConfigService();\n\n const auth = betterAuth({\n basePath: '/api/auth',\n baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',\n secret: process.env.BETTER_AUTH_SECRET,\n trustedOrigins: (process.env.CORS_ORIGINS?.split(',') || [\n 'http://localhost:5173',\n 'http://localhost:3000',\n ]),\n database: prismaAdapter(prisma, { provider: 'postgresql' }),\n emailAndPassword: { enabled: true },\n ...(hasGoogle && {\n socialProviders: {\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID ?? '',\n clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',\n },\n },\n }),\n session: {\n cookieCache: {\n enabled: true,\n maxAge: 5 * 60, // 5 minutes\n },\n },\n databaseHooks: {\n user: {\n create: {\n after: async (user) => {\n // Auto-create a default organization for new users\n const orgName = `${user.name}'s workspace`;\n const baseSlug = `${toSlug(user.name)}-workspace`;\n\n // Ensure unique slug\n let slug = baseSlug;\n let suffix = 0;\n while (true) {\n const existing = await prisma.organization.findUnique({ where: { slug } });\n if (!existing) break;\n suffix++;\n slug = `${baseSlug}-${suffix}`;\n }\n\n const orgId = crypto.randomUUID();\n const memberId = crypto.randomUUID();\n\n await prisma.organization.create({\n data: {\n id: orgId,\n name: orgName,\n slug,\n },\n });\n\n await prisma.member.create({\n data: {\n id: memberId,\n organizationId: orgId,\n userId: user.id,\n role: 'owner',\n },\n });\n },\n },\n },\n },\n plugins: [\n organization(),\n mcp({\n loginPage: resolveMcpLoginPageUrl(configService),\n }),\n ],\n });\n return auth as unknown as AuthInstance;\n}\n"],"names":["ConfigService","betterAuth","prismaAdapter","mcp","organization","resolveMcpLoginPageUrl","toSlug","name","toLowerCase","replace","createAuth","prisma","hasGoogle","process","env","GOOGLE_CLIENT_ID","GOOGLE_CLIENT_SECRET","configService","auth","basePath","baseURL","BETTER_AUTH_URL","secret","BETTER_AUTH_SECRET","trustedOrigins","CORS_ORIGINS","split","database","provider","emailAndPassword","enabled","socialProviders","google","clientId","clientSecret","session","cookieCache","maxAge","databaseHooks","user","create","after","orgName","baseSlug","slug","suffix","existing","findUnique","where","orgId","crypto","randomUUID","memberId","data","id","member","organizationId","userId","role","plugins","loginPage"],"mappings":"AAAA;;;;;;CAMC,GAGD,SAASA,aAAa,QAAQ,iBAAiB;AAC/C,SAASC,UAAU,QAAQ,cAAc;AACzC,SAASC,aAAa,QAAQ,8BAA8B;AAC5D,SAASC,GAAG,QAAQ,sBAAsB;AAC1C,SAASC,YAAY,QAAQ,mCAAmC;AAChE,SAASC,sBAAsB,QAAQ,uBAAuB;AAe9D;;CAEC,GACD,SAASC,OAAOC,IAAY;IAC1B,OAAOA,KACJC,WAAW,GACXC,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,UAAU;AACvB;AAEA,OAAO,SAASC,WAAWC,MAAoB;IAC7C,MAAMC,YAAY,CAAC,CAACC,QAAQC,GAAG,CAACC,gBAAgB,IAAI,CAAC,CAACF,QAAQC,GAAG,CAACE,oBAAoB;IACtF,MAAMC,gBAAgB,IAAIjB;IAE1B,MAAMkB,OAAOjB,WAAW;QACtBkB,UAAU;QACVC,SAASP,QAAQC,GAAG,CAACO,eAAe,IAAI;QACxCC,QAAQT,QAAQC,GAAG,CAACS,kBAAkB;QACtCC,gBAAiBX,QAAQC,GAAG,CAACW,YAAY,EAAEC,MAAM,QAAQ;YACvD;YACA;SACD;QACDC,UAAUzB,cAAcS,QAAQ;YAAEiB,UAAU;QAAa;QACzDC,kBAAkB;YAAEC,SAAS;QAAK;QAClC,GAAIlB,aAAa;YACfmB,iBAAiB;gBACfC,QAAQ;oBACNC,UAAUpB,QAAQC,GAAG,CAACC,gBAAgB,IAAI;oBAC1CmB,cAAcrB,QAAQC,GAAG,CAACE,oBAAoB,IAAI;gBACpD;YACF;QACF,CAAC;QACDmB,SAAS;YACPC,aAAa;gBACXN,SAAS;gBACTO,QAAQ,IAAI;YACd;QACF;QACAC,eAAe;YACbC,MAAM;gBACJC,QAAQ;oBACNC,OAAO,OAAOF;wBACZ,mDAAmD;wBACnD,MAAMG,UAAU,GAAGH,KAAKhC,IAAI,CAAC,YAAY,CAAC;wBAC1C,MAAMoC,WAAW,GAAGrC,OAAOiC,KAAKhC,IAAI,EAAE,UAAU,CAAC;wBAEjD,qBAAqB;wBACrB,IAAIqC,OAAOD;wBACX,IAAIE,SAAS;wBACb,MAAO,KAAM;4BACX,MAAMC,WAAW,MAAMnC,OAAOP,YAAY,CAAC2C,UAAU,CAAC;gCAAEC,OAAO;oCAAEJ;gCAAK;4BAAE;4BACxE,IAAI,CAACE,UAAU;4BACfD;4BACAD,OAAO,GAAGD,SAAS,CAAC,EAAEE,QAAQ;wBAChC;wBAEA,MAAMI,QAAQC,OAAOC,UAAU;wBAC/B,MAAMC,WAAWF,OAAOC,UAAU;wBAElC,MAAMxC,OAAOP,YAAY,CAACoC,MAAM,CAAC;4BAC/Ba,MAAM;gCACJC,IAAIL;gCACJ1C,MAAMmC;gCACNE;4BACF;wBACF;wBAEA,MAAMjC,OAAO4C,MAAM,CAACf,MAAM,CAAC;4BACzBa,MAAM;gCACJC,IAAIF;gCACJI,gBAAgBP;gCAChBQ,QAAQlB,KAAKe,EAAE;gCACfI,MAAM;4BACR;wBACF;oBACF;gBACF;YACF;QACF;QACAC,SAAS;YACPvD;YACAD,IAAI;gBACFyD,WAAWvD,uBAAuBY;YACpC;SACD;IACH;IACA,OAAOC;AACT"}
@@ -11,6 +11,7 @@ function _ts_decorate(decorators, target, key, desc) {
11
11
  */ import { Global, Module } from "@nestjs/common";
12
12
  import { AuthGuard } from "./auth.guard.js";
13
13
  import { AuthService } from "./auth.service.js";
14
+ import { McpOAuthGuard } from "./mcp-oauth.guard.js";
14
15
  export class AuthModule {
15
16
  }
16
17
  AuthModule = _ts_decorate([
@@ -18,11 +19,13 @@ AuthModule = _ts_decorate([
18
19
  Module({
19
20
  providers: [
20
21
  AuthService,
21
- AuthGuard
22
+ AuthGuard,
23
+ McpOAuthGuard
22
24
  ],
23
25
  exports: [
24
26
  AuthService,
25
- AuthGuard
27
+ AuthGuard,
28
+ McpOAuthGuard
26
29
  ]
27
30
  })
28
31
  ], AuthModule);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/auth/auth.module.ts"],"sourcesContent":["/**\n * Auth Module\n *\n * Global module providing authentication via Better Auth.\n */\n\nimport { Global, Module } from '@nestjs/common';\nimport { AuthGuard } from './auth.guard.js';\nimport { AuthService } from './auth.service.js';\n\n@Global()\n@Module({\n providers: [AuthService, AuthGuard],\n exports: [AuthService, AuthGuard],\n})\nexport class AuthModule {}\n"],"names":["Global","Module","AuthGuard","AuthService","AuthModule","providers","exports"],"mappings":";;;;;;AAAA;;;;CAIC,GAED,SAASA,MAAM,EAAEC,MAAM,QAAQ,iBAAiB;AAChD,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,WAAW,QAAQ,oBAAoB;AAOhD,OAAO,MAAMC;AAAY;;;;QAHvBC,WAAW;YAACF;YAAaD;SAAU;QACnCI,SAAS;YAACH;YAAaD;SAAU"}
1
+ {"version":3,"sources":["../../../src/modules/auth/auth.module.ts"],"sourcesContent":["/**\n * Auth Module\n *\n * Global module providing authentication via Better Auth.\n */\n\nimport { Global, Module } from '@nestjs/common';\nimport { AuthGuard } from './auth.guard.js';\nimport { AuthService } from './auth.service.js';\nimport { McpOAuthGuard } from './mcp-oauth.guard.js';\n\n@Global()\n@Module({\n providers: [AuthService, AuthGuard, McpOAuthGuard],\n exports: [AuthService, AuthGuard, McpOAuthGuard],\n})\nexport class AuthModule {}\n"],"names":["Global","Module","AuthGuard","AuthService","McpOAuthGuard","AuthModule","providers","exports"],"mappings":";;;;;;AAAA;;;;CAIC,GAED,SAASA,MAAM,EAAEC,MAAM,QAAQ,iBAAiB;AAChD,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,WAAW,QAAQ,oBAAoB;AAChD,SAASC,aAAa,QAAQ,uBAAuB;AAOrD,OAAO,MAAMC;AAAY;;;;QAHvBC,WAAW;YAACH;YAAaD;YAAWE;SAAc;QAClDG,SAAS;YAACJ;YAAaD;YAAWE;SAAc"}
@@ -80,12 +80,12 @@ export class AuthService {
80
80
  try {
81
81
  const tokenRecord = await this.prisma.oauthAccessToken.findFirst({
82
82
  where: {
83
- token: bearerToken
83
+ accessToken: bearerToken
84
84
  }
85
85
  });
86
86
  if (!tokenRecord || !tokenRecord.userId) return null;
87
87
  // Check expiration
88
- if (tokenRecord.expiresAt && new Date(tokenRecord.expiresAt) < new Date()) {
88
+ if (new Date(tokenRecord.accessTokenExpiresAt) < new Date()) {
89
89
  return null;
90
90
  }
91
91
  // Resolve user
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/auth/auth.service.ts"],"sourcesContent":["/**\n * Auth Service\n *\n * Wraps Better Auth instance for use in NestJS services and guards.\n * Auth is always enabled with email+password as baseline.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { PrismaService } from '../database/prisma.service.js';\nimport { type AuthInstance, createAuth } from './auth.config.js';\n\nexport interface AuthUser {\n id: string;\n name: string;\n email: string;\n image?: string | null;\n}\n\nexport interface AuthSession {\n id: string;\n userId: string;\n activeOrganizationId?: string | null;\n}\n\n@Injectable()\nexport class AuthService implements OnModuleInit {\n private readonly logger = new Logger(AuthService.name);\n private auth!: AuthInstance;\n\n constructor(\n private readonly prisma: PrismaService,\n private readonly configService: ConfigService\n ) {}\n\n onModuleInit() {\n if (!this.configService.get<string>('BETTER_AUTH_SECRET')) {\n const generated = randomUUID() + randomUUID();\n process.env.BETTER_AUTH_SECRET = generated;\n this.logger.warn(\n 'BETTER_AUTH_SECRET not set — using auto-generated secret. ' +\n 'Sessions will be invalidated on restart. Set BETTER_AUTH_SECRET in .env for persistence.'\n );\n }\n try {\n this.auth = createAuth(this.prisma);\n this.logger.log('Auth initialized (always enabled)');\n } catch (error) {\n this.logger.error(\n 'Failed to initialize Better Auth. Check database connectivity and migrations.',\n error instanceof Error ? error.stack : String(error)\n );\n throw error;\n }\n }\n\n /** Get the raw Better Auth instance (for mounting handler). */\n getAuth(): AuthInstance {\n return this.auth;\n }\n\n /** Validate a session from request headers (cookie-based) */\n async getSession(headers: Headers): Promise<{ user: AuthUser; session: AuthSession } | null> {\n try {\n const result = await this.auth.api.getSession({ headers });\n if (!result) return null;\n return {\n user: {\n id: result.user.id,\n name: result.user.name,\n email: result.user.email,\n image: result.user.image,\n },\n session: {\n id: result.session.id,\n userId: result.session.userId,\n activeOrganizationId: (result.session as Record<string, unknown>).activeOrganizationId as\n | string\n | null\n | undefined,\n },\n };\n } catch {\n return null;\n }\n }\n\n /** Get organizations a user belongs to */\n async getUserOrganizations(userId: string) {\n return this.prisma.member.findMany({\n where: { userId },\n include: { organization: true },\n });\n }\n\n /**\n * Validate an MCP OAuth Bearer token from the proxy endpoint.\n * Returns the user if the token is valid, null otherwise.\n */\n async validateMcpToken(bearerToken: string): Promise<AuthUser | null> {\n try {\n const tokenRecord = await this.prisma.oauthAccessToken.findFirst({\n where: { token: bearerToken },\n });\n\n if (!tokenRecord || !tokenRecord.userId) return null;\n\n // Check expiration\n if (tokenRecord.expiresAt && new Date(tokenRecord.expiresAt) < new Date()) {\n return null;\n }\n\n // Resolve user\n const user = await this.prisma.user.findUnique({\n where: { id: tokenRecord.userId },\n });\n\n if (!user) return null;\n\n return {\n id: user.id,\n name: user.name,\n email: user.email,\n image: user.image,\n };\n } catch {\n return null;\n }\n }\n}\n"],"names":["randomUUID","Injectable","Logger","ConfigService","PrismaService","createAuth","AuthService","prisma","configService","logger","name","onModuleInit","get","generated","process","env","BETTER_AUTH_SECRET","warn","auth","log","error","Error","stack","String","getAuth","getSession","headers","result","api","user","id","email","image","session","userId","activeOrganizationId","getUserOrganizations","member","findMany","where","include","organization","validateMcpToken","bearerToken","tokenRecord","oauthAccessToken","findFirst","token","expiresAt","Date","findUnique"],"mappings":";;;;;;;;;AAAA;;;;;CAKC,GAED,SAASA,UAAU,QAAQ,cAAc;AACzC,SAASC,UAAU,EAAEC,MAAM,QAAsB,iBAAiB;AAClE,SAASC,aAAa,QAAQ,iBAAiB;AAC/C,SAASC,aAAa,QAAQ,gCAAgC;AAC9D,SAA4BC,UAAU,QAAQ,mBAAmB;AAgBjE,OAAO,MAAMC;IAIX,YACE,AAAiBC,MAAqB,EACtC,AAAiBC,aAA4B,CAC7C;aAFiBD,SAAAA;aACAC,gBAAAA;aALFC,SAAS,IAAIP,OAAOI,YAAYI,IAAI;IAMlD;IAEHC,eAAe;QACb,IAAI,CAAC,IAAI,CAACH,aAAa,CAACI,GAAG,CAAS,uBAAuB;YACzD,MAAMC,YAAYb,eAAeA;YACjCc,QAAQC,GAAG,CAACC,kBAAkB,GAAGH;YACjC,IAAI,CAACJ,MAAM,CAACQ,IAAI,CACd,+DACE;QAEN;QACA,IAAI;YACF,IAAI,CAACC,IAAI,GAAGb,WAAW,IAAI,CAACE,MAAM;YAClC,IAAI,CAACE,MAAM,CAACU,GAAG,CAAC;QAClB,EAAE,OAAOC,OAAO;YACd,IAAI,CAACX,MAAM,CAACW,KAAK,CACf,iFACAA,iBAAiBC,QAAQD,MAAME,KAAK,GAAGC,OAAOH;YAEhD,MAAMA;QACR;IACF;IAEA,6DAA6D,GAC7DI,UAAwB;QACtB,OAAO,IAAI,CAACN,IAAI;IAClB;IAEA,2DAA2D,GAC3D,MAAMO,WAAWC,OAAgB,EAA4D;QAC3F,IAAI;YACF,MAAMC,SAAS,MAAM,IAAI,CAACT,IAAI,CAACU,GAAG,CAACH,UAAU,CAAC;gBAAEC;YAAQ;YACxD,IAAI,CAACC,QAAQ,OAAO;YACpB,OAAO;gBACLE,MAAM;oBACJC,IAAIH,OAAOE,IAAI,CAACC,EAAE;oBAClBpB,MAAMiB,OAAOE,IAAI,CAACnB,IAAI;oBACtBqB,OAAOJ,OAAOE,IAAI,CAACE,KAAK;oBACxBC,OAAOL,OAAOE,IAAI,CAACG,KAAK;gBAC1B;gBACAC,SAAS;oBACPH,IAAIH,OAAOM,OAAO,CAACH,EAAE;oBACrBI,QAAQP,OAAOM,OAAO,CAACC,MAAM;oBAC7BC,sBAAsB,AAACR,OAAOM,OAAO,CAA6BE,oBAAoB;gBAIxF;YACF;QACF,EAAE,OAAM;YACN,OAAO;QACT;IACF;IAEA,wCAAwC,GACxC,MAAMC,qBAAqBF,MAAc,EAAE;QACzC,OAAO,IAAI,CAAC3B,MAAM,CAAC8B,MAAM,CAACC,QAAQ,CAAC;YACjCC,OAAO;gBAAEL;YAAO;YAChBM,SAAS;gBAAEC,cAAc;YAAK;QAChC;IACF;IAEA;;;GAGC,GACD,MAAMC,iBAAiBC,WAAmB,EAA4B;QACpE,IAAI;YACF,MAAMC,cAAc,MAAM,IAAI,CAACrC,MAAM,CAACsC,gBAAgB,CAACC,SAAS,CAAC;gBAC/DP,OAAO;oBAAEQ,OAAOJ;gBAAY;YAC9B;YAEA,IAAI,CAACC,eAAe,CAACA,YAAYV,MAAM,EAAE,OAAO;YAEhD,mBAAmB;YACnB,IAAIU,YAAYI,SAAS,IAAI,IAAIC,KAAKL,YAAYI,SAAS,IAAI,IAAIC,QAAQ;gBACzE,OAAO;YACT;YAEA,eAAe;YACf,MAAMpB,OAAO,MAAM,IAAI,CAACtB,MAAM,CAACsB,IAAI,CAACqB,UAAU,CAAC;gBAC7CX,OAAO;oBAAET,IAAIc,YAAYV,MAAM;gBAAC;YAClC;YAEA,IAAI,CAACL,MAAM,OAAO;YAElB,OAAO;gBACLC,IAAID,KAAKC,EAAE;gBACXpB,MAAMmB,KAAKnB,IAAI;gBACfqB,OAAOF,KAAKE,KAAK;gBACjBC,OAAOH,KAAKG,KAAK;YACnB;QACF,EAAE,OAAM;YACN,OAAO;QACT;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/modules/auth/auth.service.ts"],"sourcesContent":["/**\n * Auth Service\n *\n * Wraps Better Auth instance for use in NestJS services and guards.\n * Auth is always enabled with email+password as baseline.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { PrismaService } from '../database/prisma.service.js';\nimport { type AuthInstance, createAuth } from './auth.config.js';\n\nexport interface AuthUser {\n id: string;\n name: string;\n email: string;\n image?: string | null;\n}\n\nexport interface AuthSession {\n id: string;\n userId: string;\n activeOrganizationId?: string | null;\n}\n\n@Injectable()\nexport class AuthService implements OnModuleInit {\n private readonly logger = new Logger(AuthService.name);\n private auth!: AuthInstance;\n\n constructor(\n private readonly prisma: PrismaService,\n private readonly configService: ConfigService\n ) {}\n\n onModuleInit() {\n if (!this.configService.get<string>('BETTER_AUTH_SECRET')) {\n const generated = randomUUID() + randomUUID();\n process.env.BETTER_AUTH_SECRET = generated;\n this.logger.warn(\n 'BETTER_AUTH_SECRET not set — using auto-generated secret. ' +\n 'Sessions will be invalidated on restart. Set BETTER_AUTH_SECRET in .env for persistence.'\n );\n }\n try {\n this.auth = createAuth(this.prisma);\n this.logger.log('Auth initialized (always enabled)');\n } catch (error) {\n this.logger.error(\n 'Failed to initialize Better Auth. Check database connectivity and migrations.',\n error instanceof Error ? error.stack : String(error)\n );\n throw error;\n }\n }\n\n /** Get the raw Better Auth instance (for mounting handler). */\n getAuth(): AuthInstance {\n return this.auth;\n }\n\n /** Validate a session from request headers (cookie-based) */\n async getSession(headers: Headers): Promise<{ user: AuthUser; session: AuthSession } | null> {\n try {\n const result = await this.auth.api.getSession({ headers });\n if (!result) return null;\n return {\n user: {\n id: result.user.id,\n name: result.user.name,\n email: result.user.email,\n image: result.user.image,\n },\n session: {\n id: result.session.id,\n userId: result.session.userId,\n activeOrganizationId: (result.session as Record<string, unknown>).activeOrganizationId as\n | string\n | null\n | undefined,\n },\n };\n } catch {\n return null;\n }\n }\n\n /** Get organizations a user belongs to */\n async getUserOrganizations(userId: string) {\n return this.prisma.member.findMany({\n where: { userId },\n include: { organization: true },\n });\n }\n\n /**\n * Validate an MCP OAuth Bearer token from the proxy endpoint.\n * Returns the user if the token is valid, null otherwise.\n */\n async validateMcpToken(bearerToken: string): Promise<AuthUser | null> {\n try {\n const tokenRecord = await this.prisma.oauthAccessToken.findFirst({\n where: { accessToken: bearerToken },\n });\n\n if (!tokenRecord || !tokenRecord.userId) return null;\n\n // Check expiration\n if (new Date(tokenRecord.accessTokenExpiresAt) < new Date()) {\n return null;\n }\n\n // Resolve user\n const user = await this.prisma.user.findUnique({\n where: { id: tokenRecord.userId },\n });\n\n if (!user) return null;\n\n return {\n id: user.id,\n name: user.name,\n email: user.email,\n image: user.image,\n };\n } catch {\n return null;\n }\n }\n}\n"],"names":["randomUUID","Injectable","Logger","ConfigService","PrismaService","createAuth","AuthService","prisma","configService","logger","name","onModuleInit","get","generated","process","env","BETTER_AUTH_SECRET","warn","auth","log","error","Error","stack","String","getAuth","getSession","headers","result","api","user","id","email","image","session","userId","activeOrganizationId","getUserOrganizations","member","findMany","where","include","organization","validateMcpToken","bearerToken","tokenRecord","oauthAccessToken","findFirst","accessToken","Date","accessTokenExpiresAt","findUnique"],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ /**
11
+ * MCP OAuth Guard
12
+ *
13
+ * Enforces OAuth 2.1 Bearer token authentication on MCP proxy endpoints.
14
+ * Validates Bearer tokens and returns RFC 9728-compliant
15
+ * WWW-Authenticate headers to guide MCP clients through OAuth discovery.
16
+ */ import { Injectable, UnauthorizedException } from "@nestjs/common";
17
+ import { ConfigService } from "@nestjs/config";
18
+ import { AuthService } from "./auth.service.js";
19
+ import { createMcpWwwAuthenticateHeader } from "./mcp-oauth.utils.js";
20
+ export class McpOAuthGuard {
21
+ constructor(authService, configService){
22
+ this.authService = authService;
23
+ this.configService = configService;
24
+ }
25
+ async canActivate(context) {
26
+ const request = context.switchToHttp().getRequest();
27
+ const token = this.extractToken(request);
28
+ if (!token) {
29
+ throw this.createUnauthorizedError('Bearer token required');
30
+ }
31
+ const user = await this.authService.validateMcpToken(token);
32
+ if (!user) {
33
+ throw this.createUnauthorizedError('Invalid or expired MCP OAuth token');
34
+ }
35
+ request.user = user;
36
+ return true;
37
+ }
38
+ /**
39
+ * Extract Bearer token from Authorization header or access_token query param (SSE fallback).
40
+ */ extractToken(request) {
41
+ const authHeader = request.headers.authorization;
42
+ if (authHeader?.startsWith('Bearer ')) {
43
+ return authHeader.slice(7);
44
+ }
45
+ // SSE connections may pass token as query parameter
46
+ const queryToken = request.query.access_token;
47
+ if (typeof queryToken === 'string' && queryToken.length > 0) {
48
+ return queryToken;
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * Create UnauthorizedException with RFC 9728 WWW-Authenticate header
54
+ * pointing MCP clients to the protected resource metadata.
55
+ */ createUnauthorizedError(message) {
56
+ const error = new UnauthorizedException(message);
57
+ error.wwwAuthenticate = createMcpWwwAuthenticateHeader(this.configService);
58
+ return error;
59
+ }
60
+ }
61
+ McpOAuthGuard = _ts_decorate([
62
+ Injectable(),
63
+ _ts_metadata("design:type", Function),
64
+ _ts_metadata("design:paramtypes", [
65
+ typeof AuthService === "undefined" ? Object : AuthService,
66
+ typeof ConfigService === "undefined" ? Object : ConfigService
67
+ ])
68
+ ], McpOAuthGuard);
69
+
70
+ //# sourceMappingURL=mcp-oauth.guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/modules/auth/mcp-oauth.guard.ts"],"sourcesContent":["/**\n * MCP OAuth Guard\n *\n * Enforces OAuth 2.1 Bearer token authentication on MCP proxy endpoints.\n * Validates Bearer tokens and returns RFC 9728-compliant\n * WWW-Authenticate headers to guide MCP clients through OAuth discovery.\n */\n\nimport {\n CanActivate,\n ExecutionContext,\n Injectable,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport type { Request } from 'express';\nimport { AuthService } from './auth.service.js';\nimport { createMcpWwwAuthenticateHeader } from './mcp-oauth.utils.js';\n\ntype McpUnauthorizedException = UnauthorizedException & {\n wwwAuthenticate?: string;\n};\n\n@Injectable()\nexport class McpOAuthGuard implements CanActivate {\n constructor(\n private readonly authService: AuthService,\n private readonly configService: ConfigService\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<Request>();\n const token = this.extractToken(request);\n\n if (!token) {\n throw this.createUnauthorizedError('Bearer token required');\n }\n\n const user = await this.authService.validateMcpToken(token);\n if (!user) {\n throw this.createUnauthorizedError('Invalid or expired MCP OAuth token');\n }\n\n request.user = user;\n return true;\n }\n\n /**\n * Extract Bearer token from Authorization header or access_token query param (SSE fallback).\n */\n private extractToken(request: Request): string | null {\n const authHeader = request.headers.authorization;\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7);\n }\n\n // SSE connections may pass token as query parameter\n const queryToken = request.query.access_token;\n if (typeof queryToken === 'string' && queryToken.length > 0) {\n return queryToken;\n }\n\n return null;\n }\n\n /**\n * Create UnauthorizedException with RFC 9728 WWW-Authenticate header\n * pointing MCP clients to the protected resource metadata.\n */\n private createUnauthorizedError(message: string): UnauthorizedException {\n const error = new UnauthorizedException(message) as McpUnauthorizedException;\n error.wwwAuthenticate = createMcpWwwAuthenticateHeader(this.configService);\n return error;\n }\n}\n"],"names":["Injectable","UnauthorizedException","ConfigService","AuthService","createMcpWwwAuthenticateHeader","McpOAuthGuard","authService","configService","canActivate","context","request","switchToHttp","getRequest","token","extractToken","createUnauthorizedError","user","validateMcpToken","authHeader","headers","authorization","startsWith","slice","queryToken","query","access_token","length","message","error","wwwAuthenticate"],"mappings":";;;;;;;;;AAAA;;;;;;CAMC,GAED,SAGEA,UAAU,EACVC,qBAAqB,QAChB,iBAAiB;AACxB,SAASC,aAAa,QAAQ,iBAAiB;AAE/C,SAASC,WAAW,QAAQ,oBAAoB;AAChD,SAASC,8BAA8B,QAAQ,uBAAuB;AAOtE,OAAO,MAAMC;IACX,YACE,AAAiBC,WAAwB,EACzC,AAAiBC,aAA4B,CAC7C;aAFiBD,cAAAA;aACAC,gBAAAA;IAChB;IAEH,MAAMC,YAAYC,OAAyB,EAAoB;QAC7D,MAAMC,UAAUD,QAAQE,YAAY,GAAGC,UAAU;QACjD,MAAMC,QAAQ,IAAI,CAACC,YAAY,CAACJ;QAEhC,IAAI,CAACG,OAAO;YACV,MAAM,IAAI,CAACE,uBAAuB,CAAC;QACrC;QAEA,MAAMC,OAAO,MAAM,IAAI,CAACV,WAAW,CAACW,gBAAgB,CAACJ;QACrD,IAAI,CAACG,MAAM;YACT,MAAM,IAAI,CAACD,uBAAuB,CAAC;QACrC;QAEAL,QAAQM,IAAI,GAAGA;QACf,OAAO;IACT;IAEA;;GAEC,GACD,AAAQF,aAAaJ,OAAgB,EAAiB;QACpD,MAAMQ,aAAaR,QAAQS,OAAO,CAACC,aAAa;QAChD,IAAIF,YAAYG,WAAW,YAAY;YACrC,OAAOH,WAAWI,KAAK,CAAC;QAC1B;QAEA,oDAAoD;QACpD,MAAMC,aAAab,QAAQc,KAAK,CAACC,YAAY;QAC7C,IAAI,OAAOF,eAAe,YAAYA,WAAWG,MAAM,GAAG,GAAG;YAC3D,OAAOH;QACT;QAEA,OAAO;IACT;IAEA;;;GAGC,GACD,AAAQR,wBAAwBY,OAAe,EAAyB;QACtE,MAAMC,QAAQ,IAAI3B,sBAAsB0B;QACxCC,MAAMC,eAAe,GAAGzB,+BAA+B,IAAI,CAACG,aAAa;QACzE,OAAOqB;IACT;AACF"}
@@ -0,0 +1,75 @@
1
+ const DEFAULT_BACKEND_PORT = '3001';
2
+ const DEFAULT_FRONTEND_PORT = '3000';
3
+ function trimTrailingSlash(value) {
4
+ return value.replace(/\/+$/, '');
5
+ }
6
+ function swapPort(origin, fromPort, toPort) {
7
+ const url = new URL(origin);
8
+ if (url.port === fromPort) {
9
+ url.port = toPort;
10
+ }
11
+ return trimTrailingSlash(url.origin);
12
+ }
13
+ export function resolvePublicBackendOrigin(configService) {
14
+ const configuredUrl = configService.get('BETTER_AUTH_URL');
15
+ if (configuredUrl) {
16
+ return trimTrailingSlash(new URL(configuredUrl).origin);
17
+ }
18
+ const port = configService.get('app.port') || Number(DEFAULT_BACKEND_PORT);
19
+ return `http://localhost:${port}`;
20
+ }
21
+ export function resolvePublicAuthBaseUrl(configService) {
22
+ return `${resolvePublicBackendOrigin(configService)}/api/auth`;
23
+ }
24
+ export function resolveFrontendOrigin(configService) {
25
+ const configuredUrl = configService.get('FRONTEND_URL');
26
+ if (configuredUrl) {
27
+ return trimTrailingSlash(new URL(configuredUrl).origin);
28
+ }
29
+ const backendOrigin = resolvePublicBackendOrigin(configService);
30
+ const backendUrl = new URL(backendOrigin);
31
+ if (backendUrl.port === '9631') {
32
+ return swapPort(backendOrigin, '9631', '9630');
33
+ }
34
+ if (backendUrl.port === DEFAULT_BACKEND_PORT) {
35
+ return swapPort(backendOrigin, DEFAULT_BACKEND_PORT, DEFAULT_FRONTEND_PORT);
36
+ }
37
+ return trimTrailingSlash(backendUrl.origin);
38
+ }
39
+ export function resolveMcpLoginPageUrl(configService) {
40
+ return `${resolveFrontendOrigin(configService)}/sign-in`;
41
+ }
42
+ export function createMcpProtectedResourceMetadata(configService) {
43
+ const backendOrigin = resolvePublicBackendOrigin(configService);
44
+ const authBaseUrl = resolvePublicAuthBaseUrl(configService);
45
+ return {
46
+ resource: `${backendOrigin}/api/mcp`,
47
+ authorization_servers: [
48
+ backendOrigin
49
+ ],
50
+ bearer_methods_supported: [
51
+ 'header'
52
+ ],
53
+ scopes_supported: [
54
+ 'openid',
55
+ 'profile',
56
+ 'email',
57
+ 'offline_access'
58
+ ],
59
+ jwks_uri: `${authBaseUrl}/mcp/jwks`,
60
+ resource_signing_alg_values_supported: [
61
+ 'RS256',
62
+ 'none'
63
+ ]
64
+ };
65
+ }
66
+ export function createMcpWwwAuthenticateHeader(configService) {
67
+ const resourceMetadataUrl = `${resolvePublicBackendOrigin(configService)}/.well-known/oauth-protected-resource`;
68
+ return [
69
+ 'Bearer',
70
+ `resource_metadata="${resourceMetadataUrl}"`,
71
+ `resource_metadata_uri="${resourceMetadataUrl}"`
72
+ ].join(' ');
73
+ }
74
+
75
+ //# sourceMappingURL=mcp-oauth.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/modules/auth/mcp-oauth.utils.ts"],"sourcesContent":["import type { ConfigService } from '@nestjs/config';\n\nconst DEFAULT_BACKEND_PORT = '3001';\nconst DEFAULT_FRONTEND_PORT = '3000';\n\nfunction trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, '');\n}\n\nfunction swapPort(origin: string, fromPort: string, toPort: string): string {\n const url = new URL(origin);\n if (url.port === fromPort) {\n url.port = toPort;\n }\n return trimTrailingSlash(url.origin);\n}\n\nexport function resolvePublicBackendOrigin(configService: ConfigService): string {\n const configuredUrl = configService.get<string>('BETTER_AUTH_URL');\n if (configuredUrl) {\n return trimTrailingSlash(new URL(configuredUrl).origin);\n }\n\n const port = configService.get<number>('app.port') || Number(DEFAULT_BACKEND_PORT);\n return `http://localhost:${port}`;\n}\n\nexport function resolvePublicAuthBaseUrl(configService: ConfigService): string {\n return `${resolvePublicBackendOrigin(configService)}/api/auth`;\n}\n\nexport function resolveFrontendOrigin(configService: ConfigService): string {\n const configuredUrl = configService.get<string>('FRONTEND_URL');\n if (configuredUrl) {\n return trimTrailingSlash(new URL(configuredUrl).origin);\n }\n\n const backendOrigin = resolvePublicBackendOrigin(configService);\n const backendUrl = new URL(backendOrigin);\n\n if (backendUrl.port === '9631') {\n return swapPort(backendOrigin, '9631', '9630');\n }\n\n if (backendUrl.port === DEFAULT_BACKEND_PORT) {\n return swapPort(backendOrigin, DEFAULT_BACKEND_PORT, DEFAULT_FRONTEND_PORT);\n }\n\n return trimTrailingSlash(backendUrl.origin);\n}\n\nexport function resolveMcpLoginPageUrl(configService: ConfigService): string {\n return `${resolveFrontendOrigin(configService)}/sign-in`;\n}\n\nexport function createMcpProtectedResourceMetadata(configService: ConfigService) {\n const backendOrigin = resolvePublicBackendOrigin(configService);\n const authBaseUrl = resolvePublicAuthBaseUrl(configService);\n\n return {\n resource: `${backendOrigin}/api/mcp`,\n authorization_servers: [backendOrigin],\n bearer_methods_supported: ['header'],\n scopes_supported: ['openid', 'profile', 'email', 'offline_access'],\n jwks_uri: `${authBaseUrl}/mcp/jwks`,\n resource_signing_alg_values_supported: ['RS256', 'none'],\n };\n}\n\nexport function createMcpWwwAuthenticateHeader(configService: ConfigService): string {\n const resourceMetadataUrl = `${resolvePublicBackendOrigin(\n configService\n )}/.well-known/oauth-protected-resource`;\n\n return [\n 'Bearer',\n `resource_metadata=\"${resourceMetadataUrl}\"`,\n `resource_metadata_uri=\"${resourceMetadataUrl}\"`,\n ].join(' ');\n}\n"],"names":["DEFAULT_BACKEND_PORT","DEFAULT_FRONTEND_PORT","trimTrailingSlash","value","replace","swapPort","origin","fromPort","toPort","url","URL","port","resolvePublicBackendOrigin","configService","configuredUrl","get","Number","resolvePublicAuthBaseUrl","resolveFrontendOrigin","backendOrigin","backendUrl","resolveMcpLoginPageUrl","createMcpProtectedResourceMetadata","authBaseUrl","resource","authorization_servers","bearer_methods_supported","scopes_supported","jwks_uri","resource_signing_alg_values_supported","createMcpWwwAuthenticateHeader","resourceMetadataUrl","join"],"mappings":"AAEA,MAAMA,uBAAuB;AAC7B,MAAMC,wBAAwB;AAE9B,SAASC,kBAAkBC,KAAa;IACtC,OAAOA,MAAMC,OAAO,CAAC,QAAQ;AAC/B;AAEA,SAASC,SAASC,MAAc,EAAEC,QAAgB,EAAEC,MAAc;IAChE,MAAMC,MAAM,IAAIC,IAAIJ;IACpB,IAAIG,IAAIE,IAAI,KAAKJ,UAAU;QACzBE,IAAIE,IAAI,GAAGH;IACb;IACA,OAAON,kBAAkBO,IAAIH,MAAM;AACrC;AAEA,OAAO,SAASM,2BAA2BC,aAA4B;IACrE,MAAMC,gBAAgBD,cAAcE,GAAG,CAAS;IAChD,IAAID,eAAe;QACjB,OAAOZ,kBAAkB,IAAIQ,IAAII,eAAeR,MAAM;IACxD;IAEA,MAAMK,OAAOE,cAAcE,GAAG,CAAS,eAAeC,OAAOhB;IAC7D,OAAO,CAAC,iBAAiB,EAAEW,MAAM;AACnC;AAEA,OAAO,SAASM,yBAAyBJ,aAA4B;IACnE,OAAO,GAAGD,2BAA2BC,eAAe,SAAS,CAAC;AAChE;AAEA,OAAO,SAASK,sBAAsBL,aAA4B;IAChE,MAAMC,gBAAgBD,cAAcE,GAAG,CAAS;IAChD,IAAID,eAAe;QACjB,OAAOZ,kBAAkB,IAAIQ,IAAII,eAAeR,MAAM;IACxD;IAEA,MAAMa,gBAAgBP,2BAA2BC;IACjD,MAAMO,aAAa,IAAIV,IAAIS;IAE3B,IAAIC,WAAWT,IAAI,KAAK,QAAQ;QAC9B,OAAON,SAASc,eAAe,QAAQ;IACzC;IAEA,IAAIC,WAAWT,IAAI,KAAKX,sBAAsB;QAC5C,OAAOK,SAASc,eAAenB,sBAAsBC;IACvD;IAEA,OAAOC,kBAAkBkB,WAAWd,MAAM;AAC5C;AAEA,OAAO,SAASe,uBAAuBR,aAA4B;IACjE,OAAO,GAAGK,sBAAsBL,eAAe,QAAQ,CAAC;AAC1D;AAEA,OAAO,SAASS,mCAAmCT,aAA4B;IAC7E,MAAMM,gBAAgBP,2BAA2BC;IACjD,MAAMU,cAAcN,yBAAyBJ;IAE7C,OAAO;QACLW,UAAU,GAAGL,cAAc,QAAQ,CAAC;QACpCM,uBAAuB;YAACN;SAAc;QACtCO,0BAA0B;YAAC;SAAS;QACpCC,kBAAkB;YAAC;YAAU;YAAW;YAAS;SAAiB;QAClEC,UAAU,GAAGL,YAAY,SAAS,CAAC;QACnCM,uCAAuC;YAAC;YAAS;SAAO;IAC1D;AACF;AAEA,OAAO,SAASC,+BAA+BjB,aAA4B;IACzE,MAAMkB,sBAAsB,GAAGnB,2BAC7BC,eACA,qCAAqC,CAAC;IAExC,OAAO;QACL;QACA,CAAC,mBAAmB,EAAEkB,oBAAoB,CAAC,CAAC;QAC5C,CAAC,uBAAuB,EAAEA,oBAAoB,CAAC,CAAC;KACjD,CAACC,IAAI,CAAC;AACT"}
@@ -253,7 +253,8 @@ export class McpService {
253
253
  const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
254
254
  const remoteServer = new RemoteHttpMcpServer({
255
255
  url: config.url,
256
- transport: 'http'
256
+ transport: 'http',
257
+ headers: config.headers
257
258
  }, null, apiKeyConfig);
258
259
  await remoteServer.initialize();
259
260
  const tools = await remoteServer.listTools();
@@ -267,7 +268,8 @@ export class McpService {
267
268
  const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
268
269
  const remoteServer = new RemoteSseMcpServer({
269
270
  url: config.url,
270
- transport: 'sse'
271
+ transport: 'sse',
272
+ headers: config.headers
271
273
  }, null, apiKeyConfig);
272
274
  await remoteServer.initialize();
273
275
  const tools = await remoteServer.listTools();
@@ -382,14 +384,28 @@ export class McpService {
382
384
  validationDetails = 'Server ready (no API key required)';
383
385
  }
384
386
  // For remote_http servers, validate by connecting
387
+ let oauthRequired = false;
385
388
  if (server.type === 'remote_http' && status === 'unknown') {
386
389
  const config = this.parseConfig(server.config);
387
390
  const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
391
+ // Get OAuth token if available, mapping Prisma types to core OAuthToken
392
+ const oauthToken = server.oauthToken ? {
393
+ id: server.oauthToken.id,
394
+ mcpServerId: server.oauthToken.mcpServerId,
395
+ accessToken: server.oauthToken.accessToken,
396
+ tokenType: server.oauthToken.tokenType,
397
+ refreshToken: server.oauthToken.refreshToken ?? undefined,
398
+ scope: server.oauthToken.scope ?? undefined,
399
+ expiresAt: server.oauthToken.expiresAt?.getTime(),
400
+ createdAt: server.oauthToken.createdAt.getTime(),
401
+ updatedAt: server.oauthToken.updatedAt.getTime()
402
+ } : null;
388
403
  try {
389
404
  const remoteServer = new RemoteHttpMcpServer({
390
405
  url: config.url,
391
- transport: 'http'
392
- }, null, apiKeyConfig);
406
+ transport: 'http',
407
+ headers: config.headers
408
+ }, oauthToken, apiKeyConfig);
393
409
  await remoteServer.initialize();
394
410
  const tools = await remoteServer.listTools();
395
411
  status = 'connected';
@@ -397,18 +413,36 @@ export class McpService {
397
413
  } catch (error) {
398
414
  status = 'error';
399
415
  validationError = error instanceof Error ? error.message : 'Unknown error';
400
- validationDetails = `Connection failed: ${validationError}`;
416
+ if (validationError.includes('OAUTH_REQUIRED')) {
417
+ oauthRequired = true;
418
+ validationDetails = 'OAuth authentication required. Click "Login with OAuth" to authorize.';
419
+ } else {
420
+ validationDetails = `Connection failed: ${validationError}`;
421
+ }
401
422
  }
402
423
  }
403
424
  // For remote_sse servers, validate by connecting
404
425
  if (server.type === 'remote_sse' && status === 'unknown') {
405
426
  const config = this.parseConfig(server.config);
406
427
  const apiKeyConfig = server.apiKeyConfig ? JSON.parse(server.apiKeyConfig) : null;
428
+ // Get OAuth token if available, mapping Prisma types to core OAuthToken
429
+ const oauthToken = server.oauthToken ? {
430
+ id: server.oauthToken.id,
431
+ mcpServerId: server.oauthToken.mcpServerId,
432
+ accessToken: server.oauthToken.accessToken,
433
+ tokenType: server.oauthToken.tokenType,
434
+ refreshToken: server.oauthToken.refreshToken ?? undefined,
435
+ scope: server.oauthToken.scope ?? undefined,
436
+ expiresAt: server.oauthToken.expiresAt?.getTime(),
437
+ createdAt: server.oauthToken.createdAt.getTime(),
438
+ updatedAt: server.oauthToken.updatedAt.getTime()
439
+ } : null;
407
440
  try {
408
441
  const remoteServer = new RemoteSseMcpServer({
409
442
  url: config.url,
410
- transport: 'sse'
411
- }, null, apiKeyConfig);
443
+ transport: 'sse',
444
+ headers: config.headers
445
+ }, oauthToken, apiKeyConfig);
412
446
  await remoteServer.initialize();
413
447
  const tools = await remoteServer.listTools();
414
448
  status = 'connected';
@@ -416,7 +450,12 @@ export class McpService {
416
450
  } catch (error) {
417
451
  status = 'error';
418
452
  validationError = error instanceof Error ? error.message : 'Unknown error';
419
- validationDetails = `Connection failed: ${validationError}`;
453
+ if (validationError.includes('OAUTH_REQUIRED')) {
454
+ oauthRequired = true;
455
+ validationDetails = 'OAuth authentication required. Click "Login with OAuth" to authorize.';
456
+ } else {
457
+ validationDetails = `Connection failed: ${validationError}`;
458
+ }
420
459
  }
421
460
  }
422
461
  // For external (NPX/stdio) servers, validate by spawning and checking
@@ -451,6 +490,7 @@ export class McpService {
451
490
  hasOAuth,
452
491
  requiresApiKey,
453
492
  requiresOAuth,
493
+ oauthRequired,
454
494
  isReady,
455
495
  status,
456
496
  error: validationError,