@dxheroes/local-mcp-backend 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +52 -0
- package/dist/__tests__/integration/mcp-proxy-auth-http.test.js +283 -0
- package/dist/__tests__/integration/mcp-proxy-auth-http.test.js.map +1 -0
- package/dist/__tests__/integration/oauth-authorize-callback.test.js +122 -0
- package/dist/__tests__/integration/oauth-authorize-callback.test.js.map +1 -0
- package/dist/__tests__/integration/proxy-auth.test.js +171 -110
- package/dist/__tests__/integration/proxy-auth.test.js.map +1 -1
- package/dist/__tests__/unit/auth.guard.test.js +23 -2
- package/dist/__tests__/unit/auth.guard.test.js.map +1 -1
- package/dist/common/filters/all-exceptions.filter.js +6 -0
- package/dist/common/filters/all-exceptions.filter.js.map +1 -1
- package/dist/main.js +63 -2
- package/dist/main.js.map +1 -1
- package/dist/modules/auth/auth.config.js +10 -5
- package/dist/modules/auth/auth.config.js.map +1 -1
- package/dist/modules/auth/auth.module.js +5 -2
- package/dist/modules/auth/auth.module.js.map +1 -1
- package/dist/modules/auth/auth.service.js +2 -2
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/mcp-oauth.guard.js +95 -0
- package/dist/modules/auth/mcp-oauth.guard.js.map +1 -0
- package/dist/modules/auth/mcp-oauth.utils.js +75 -0
- package/dist/modules/auth/mcp-oauth.utils.js.map +1 -0
- package/dist/modules/health/health.controller.js +1 -1
- package/dist/modules/health/health.controller.js.map +1 -1
- package/dist/modules/mcp/mcp.service.js +48 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/oauth/oauth.controller.js +78 -1
- package/dist/modules/oauth/oauth.controller.js.map +1 -1
- package/dist/modules/oauth/oauth.service.js +197 -1
- package/dist/modules/oauth/oauth.service.js.map +1 -1
- package/dist/modules/proxy/proxy.controller.js +152 -27
- package/dist/modules/proxy/proxy.controller.js.map +1 -1
- package/dist/modules/proxy/proxy.service.js +28 -4
- package/dist/modules/proxy/proxy.service.js.map +1 -1
- package/docker-entrypoint.sh +15 -2
- package/package.json +9 -7
- package/src/__tests__/integration/mcp-proxy-auth-http.test.ts +311 -0
- package/src/__tests__/integration/oauth-authorize-callback.test.ts +155 -0
- package/src/__tests__/integration/proxy-auth.test.ts +151 -168
- package/src/__tests__/unit/auth.guard.test.ts +12 -2
- package/src/common/filters/all-exceptions.filter.ts +11 -0
- package/src/main.ts +56 -2
- package/src/modules/auth/auth.config.ts +9 -4
- package/src/modules/auth/auth.module.ts +3 -2
- package/src/modules/auth/auth.service.ts +2 -2
- package/src/modules/auth/mcp-oauth.guard.ts +102 -0
- package/src/modules/auth/mcp-oauth.utils.ts +80 -0
- package/src/modules/health/health.controller.ts +1 -1
- package/src/modules/mcp/mcp.service.ts +54 -12
- package/src/modules/oauth/oauth.controller.ts +84 -1
- package/src/modules/oauth/oauth.service.ts +218 -1
- package/src/modules/proxy/proxy.controller.ts +120 -25
- package/src/modules/proxy/proxy.service.ts +26 -4
- package/vitest.config.ts +2 -1
package/dist/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AppModule } from "./app.module.js";
|
|
|
13
13
|
import { AllExceptionsFilter } from "./common/filters/all-exceptions.filter.js";
|
|
14
14
|
import { LoggingInterceptor } from "./common/interceptors/logging.interceptor.js";
|
|
15
15
|
import { AuthService } from "./modules/auth/auth.service.js";
|
|
16
|
+
import { createMcpProtectedResourceMetadata, resolvePublicAuthBaseUrl, resolvePublicBackendOrigin } from "./modules/auth/mcp-oauth.utils.js";
|
|
16
17
|
async function bootstrap() {
|
|
17
18
|
// Determine log levels from environment
|
|
18
19
|
const logLevel = process.env.LOG_LEVEL || 'log';
|
|
@@ -42,6 +43,10 @@ async function bootstrap() {
|
|
|
42
43
|
const configService = app.get(ConfigService);
|
|
43
44
|
// Security
|
|
44
45
|
app.use(helmet());
|
|
46
|
+
app.use((_req, res, next)=>{
|
|
47
|
+
res.setHeader('X-Robots-Tag', 'noindex, nofollow');
|
|
48
|
+
next();
|
|
49
|
+
});
|
|
45
50
|
app.use(compression());
|
|
46
51
|
// CORS
|
|
47
52
|
const corsOrigins = configService.get('CORS_ORIGINS')?.split(',') || [
|
|
@@ -80,11 +85,67 @@ async function bootstrap() {
|
|
|
80
85
|
// AuthService.onModuleInit() hasn't run yet — auth initializes during app.init().
|
|
81
86
|
const authService = app.get(AuthService);
|
|
82
87
|
const expressApp = app.getHttpAdapter().getInstance();
|
|
83
|
-
const lazyAuthHandler = (req, res, next)=>{
|
|
88
|
+
const lazyAuthHandler = async (req, res, next)=>{
|
|
84
89
|
const auth = authService.getAuth();
|
|
85
90
|
if (!auth) return next();
|
|
86
|
-
|
|
91
|
+
try {
|
|
92
|
+
await toNodeHandler(auth)(req, res);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const isPrismaNotFound = error instanceof Error && 'code' in error && error.code === 'P2025';
|
|
95
|
+
if (isPrismaNotFound) {
|
|
96
|
+
// Stale session cookie — clear it and return 401
|
|
97
|
+
res.clearCookie('better-auth.session_token');
|
|
98
|
+
res.clearCookie('better-auth.session_token.sig');
|
|
99
|
+
if (!res.headersSent) {
|
|
100
|
+
res.status(401).json({
|
|
101
|
+
error: 'Session expired'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!res.headersSent) {
|
|
107
|
+
res.status(500).json({
|
|
108
|
+
error: 'Internal server error'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
87
112
|
};
|
|
113
|
+
expressApp.get('/.well-known/oauth-protected-resource', (_req, res)=>{
|
|
114
|
+
res.json(createMcpProtectedResourceMetadata(configService));
|
|
115
|
+
});
|
|
116
|
+
// RFC 8414 – OAuth 2.0 Authorization Server Metadata
|
|
117
|
+
// MCP clients fetch this to discover the correct DCR endpoint (/api/auth/mcp/register)
|
|
118
|
+
// instead of falling back to the root /register which returns 404.
|
|
119
|
+
expressApp.get('/.well-known/oauth-authorization-server', (_req, res)=>{
|
|
120
|
+
const backendOrigin = resolvePublicBackendOrigin(configService);
|
|
121
|
+
const authBaseUrl = resolvePublicAuthBaseUrl(configService);
|
|
122
|
+
res.json({
|
|
123
|
+
issuer: backendOrigin,
|
|
124
|
+
authorization_endpoint: `${authBaseUrl}/mcp/authorize`,
|
|
125
|
+
token_endpoint: `${authBaseUrl}/mcp/token`,
|
|
126
|
+
registration_endpoint: `${authBaseUrl}/mcp/register`,
|
|
127
|
+
jwks_uri: `${authBaseUrl}/mcp/jwks`,
|
|
128
|
+
response_types_supported: [
|
|
129
|
+
'code'
|
|
130
|
+
],
|
|
131
|
+
grant_types_supported: [
|
|
132
|
+
'authorization_code',
|
|
133
|
+
'refresh_token'
|
|
134
|
+
],
|
|
135
|
+
token_endpoint_auth_methods_supported: [
|
|
136
|
+
'none'
|
|
137
|
+
],
|
|
138
|
+
code_challenge_methods_supported: [
|
|
139
|
+
'S256'
|
|
140
|
+
],
|
|
141
|
+
scopes_supported: [
|
|
142
|
+
'openid',
|
|
143
|
+
'profile',
|
|
144
|
+
'email',
|
|
145
|
+
'offline_access'
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
});
|
|
88
149
|
expressApp.all('/api/auth/*splat', lazyAuthHandler);
|
|
89
150
|
expressApp.all('/.well-known/*splat', lazyAuthHandler);
|
|
90
151
|
logger.log('Better Auth routes registered (lazy) on /api/auth/* and /.well-known/*');
|
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:
|
|
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((_req: Request, res: Response, next: NextFunction) => {\n res.setHeader('X-Robots-Tag', 'noindex, nofollow');\n next();\n });\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 = async (req: Request, res: Response, next: NextFunction) => {\n const auth = authService.getAuth();\n if (!auth) return next();\n try {\n await toNodeHandler(auth)(req, res);\n } catch (error: unknown) {\n const isPrismaNotFound =\n error instanceof Error &&\n 'code' in error &&\n (error as { code: string }).code === 'P2025';\n if (isPrismaNotFound) {\n // Stale session cookie — clear it and return 401\n res.clearCookie('better-auth.session_token');\n res.clearCookie('better-auth.session_token.sig');\n if (!res.headersSent) {\n res.status(401).json({ error: 'Session expired' });\n }\n return;\n }\n if (!res.headersSent) {\n res.status(500).json({ error: 'Internal server error' });\n }\n }\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","_req","res","next","setHeader","corsOrigins","split","enableCors","origin","credentials","methods","setGlobalPrefix","useGlobalPipes","whitelist","forbidNonWhitelisted","transform","transformOptions","enableImplicitConversion","useGlobalFilters","useGlobalInterceptors","authService","expressApp","getHttpAdapter","getInstance","lazyAuthHandler","req","auth","getAuth","error","isPrismaNotFound","Error","code","clearCookie","headersSent","status","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":"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;AAEtC,OAAOC,YAAY,SAAS;AAC5B,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,mBAAmB,QAAQ,4CAA4C;AAChF,SAASC,kBAAkB,QAAQ,+CAA+C;AAClF,SAASC,WAAW,QAAQ,iCAAiC;AAC7D,SACEC,kCAAkC,EAClCC,wBAAwB,EACxBC,0BAA0B,QACrB,oCAAoC;AAE3C,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,IAAItB,OAAO;IAC1B,MAAMuB,MAAM,MAAMpB,YAAYqB,MAAM,CAACjB,WAAW;QAC9Ce,QAAQH;IACV;IAEA,MAAMM,gBAAgBF,IAAIG,GAAG,CAACxB;IAE9B,WAAW;IACXqB,IAAII,GAAG,CAACrB;IACRiB,IAAII,GAAG,CAAC,CAACC,MAAeC,KAAeC;QACrCD,IAAIE,SAAS,CAAC,gBAAgB;QAC9BD;IACF;IACAP,IAAII,GAAG,CAACtB;IAER,OAAO;IACP,MAAM2B,cAAcP,cAAcC,GAAG,CAAS,iBAAiBO,MAAM,QAAQ;QAC3E;QACA;KACD;IACDV,IAAIW,UAAU,CAAC;QACbC,QAAQH;QACRI,aAAa;QACbC,SAAS;YAAC;YAAO;YAAQ;YAAO;YAAU;YAAS;SAAU;IAC/D;IAEA,gBAAgB;IAChBd,IAAIe,eAAe,CAAC;IAEpB,eAAe;IACff,IAAIgB,cAAc,CAChB,IAAItC,eAAe;QACjBuC,WAAW;QACXC,sBAAsB;QACtBC,WAAW;QACXC,kBAAkB;YAAEC,0BAA0B;QAAK;IACrD;IAGF,iBAAiB;IACjBrB,IAAIsB,gBAAgB,CAAC,IAAIrC;IAEzB,sBAAsB;IACtBe,IAAIuB,qBAAqB,CAAC,IAAIrC;IAE9B,yEAAyE;IACzE,yEAAyE;IACzE,kFAAkF;IAClF,MAAMsC,cAAcxB,IAAIG,GAAG,CAAChB;IAC5B,MAAMsC,aAAazB,IAAI0B,cAAc,GAAGC,WAAW;IAEnD,MAAMC,kBAAkB,OAAOC,KAAcvB,KAAeC;QAC1D,MAAMuB,OAAON,YAAYO,OAAO;QAChC,IAAI,CAACD,MAAM,OAAOvB;QAClB,IAAI;YACF,MAAM1B,cAAciD,MAAMD,KAAKvB;QACjC,EAAE,OAAO0B,OAAgB;YACvB,MAAMC,mBACJD,iBAAiBE,SACjB,UAAUF,SACV,AAACA,MAA2BG,IAAI,KAAK;YACvC,IAAIF,kBAAkB;gBACpB,iDAAiD;gBACjD3B,IAAI8B,WAAW,CAAC;gBAChB9B,IAAI8B,WAAW,CAAC;gBAChB,IAAI,CAAC9B,IAAI+B,WAAW,EAAE;oBACpB/B,IAAIgC,MAAM,CAAC,KAAKC,IAAI,CAAC;wBAAEP,OAAO;oBAAkB;gBAClD;gBACA;YACF;YACA,IAAI,CAAC1B,IAAI+B,WAAW,EAAE;gBACpB/B,IAAIgC,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEP,OAAO;gBAAwB;YACxD;QACF;IACF;IAEAP,WAAWtB,GAAG,CAAC,yCAAyC,CAACE,MAAeC;QACtEA,IAAIiC,IAAI,CAACnD,mCAAmCc;IAC9C;IAEA,qDAAqD;IACrD,uFAAuF;IACvF,mEAAmE;IACnEuB,WAAWtB,GAAG,CAAC,2CAA2C,CAACE,MAAeC;QACxE,MAAMkC,gBAAgBlD,2BAA2BY;QACjD,MAAMuC,cAAcpD,yBAAyBa;QAE7CI,IAAIiC,IAAI,CAAC;YACPG,QAAQF;YACRG,wBAAwB,GAAGF,YAAY,cAAc,CAAC;YACtDG,gBAAgB,GAAGH,YAAY,UAAU,CAAC;YAC1CI,uBAAuB,GAAGJ,YAAY,aAAa,CAAC;YACpDK,UAAU,GAAGL,YAAY,SAAS,CAAC;YACnCM,0BAA0B;gBAAC;aAAO;YAClCC,uBAAuB;gBAAC;gBAAsB;aAAgB;YAC9DC,uCAAuC;gBAAC;aAAO;YAC/CC,kCAAkC;gBAAC;aAAO;YAC1CC,kBAAkB;gBAAC;gBAAU;gBAAW;gBAAS;aAAiB;QACpE;IACF;IAEA1B,WAAW2B,GAAG,CAAC,oBAAoBxB;IACnCH,WAAW2B,GAAG,CAAC,uBAAuBxB;IAEtC7B,OAAOsD,GAAG,CAAC;IAEX,MAAMC,OAAOpD,cAAcC,GAAG,CAAS,WAAW;IAClD,MAAMH,IAAIuD,MAAM,CAACD;IAEjBvD,OAAOsD,GAAG,CAAC,CAAC,4CAA4C,EAAEC,MAAM;IAChEvD,OAAOsD,GAAG,CAAC,CAAC,mCAAmC,EAAEC,KAAK,IAAI,CAAC;IAC3DvD,OAAOsD,GAAG,CAAC;AACb;AAEA9D"}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Better Auth Configuration
|
|
3
3
|
*
|
|
4
|
-
* Configures Better Auth with Prisma adapter, email+password (
|
|
5
|
-
* optional Google OAuth, Organization plugin,
|
|
4
|
+
* Configures Better Auth with Prisma adapter, email+password (toggleable via
|
|
5
|
+
* AUTH_EMAIL_PASSWORD env var), optional Google OAuth, Organization plugin,
|
|
6
|
+
* and MCP OAuth plugin.
|
|
6
7
|
* Auto-creates a default organization on user signup.
|
|
7
|
-
*/ import {
|
|
8
|
+
*/ import { ConfigService } from "@nestjs/config";
|
|
9
|
+
import { betterAuth } from "better-auth";
|
|
8
10
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
9
11
|
import { mcp } from "better-auth/plugins";
|
|
10
12
|
import { organization } from "better-auth/plugins/organization";
|
|
13
|
+
import { resolveMcpLoginPageUrl } from "./mcp-oauth.utils.js";
|
|
11
14
|
/**
|
|
12
15
|
* Generate a URL-safe slug from a name.
|
|
13
16
|
*/ function toSlug(name) {
|
|
@@ -15,6 +18,8 @@ import { organization } from "better-auth/plugins/organization";
|
|
|
15
18
|
}
|
|
16
19
|
export function createAuth(prisma) {
|
|
17
20
|
const hasGoogle = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
|
|
21
|
+
const emailPasswordEnabled = process.env.AUTH_EMAIL_PASSWORD !== 'false';
|
|
22
|
+
const configService = new ConfigService();
|
|
18
23
|
const auth = betterAuth({
|
|
19
24
|
basePath: '/api/auth',
|
|
20
25
|
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',
|
|
@@ -27,7 +32,7 @@ export function createAuth(prisma) {
|
|
|
27
32
|
provider: 'postgresql'
|
|
28
33
|
}),
|
|
29
34
|
emailAndPassword: {
|
|
30
|
-
enabled:
|
|
35
|
+
enabled: emailPasswordEnabled
|
|
31
36
|
},
|
|
32
37
|
...hasGoogle && {
|
|
33
38
|
socialProviders: {
|
|
@@ -87,7 +92,7 @@ export function createAuth(prisma) {
|
|
|
87
92
|
plugins: [
|
|
88
93
|
organization(),
|
|
89
94
|
mcp({
|
|
90
|
-
loginPage:
|
|
95
|
+
loginPage: resolveMcpLoginPageUrl(configService)
|
|
91
96
|
})
|
|
92
97
|
]
|
|
93
98
|
});
|
|
@@ -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 (
|
|
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 (toggleable via\n * AUTH_EMAIL_PASSWORD env var), optional Google OAuth, Organization plugin,\n * 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 emailPasswordEnabled = process.env.AUTH_EMAIL_PASSWORD !== 'false';\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: emailPasswordEnabled },\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","emailPasswordEnabled","AUTH_EMAIL_PASSWORD","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;;;;;;;CAOC,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,uBAAuBJ,QAAQC,GAAG,CAACI,mBAAmB,KAAK;IACjE,MAAMC,gBAAgB,IAAInB;IAE1B,MAAMoB,OAAOnB,WAAW;QACtBoB,UAAU;QACVC,SAAST,QAAQC,GAAG,CAACS,eAAe,IAAI;QACxCC,QAAQX,QAAQC,GAAG,CAACW,kBAAkB;QACtCC,gBAAiBb,QAAQC,GAAG,CAACa,YAAY,EAAEC,MAAM,QAAQ;YACvD;YACA;SACD;QACDC,UAAU3B,cAAcS,QAAQ;YAAEmB,UAAU;QAAa;QACzDC,kBAAkB;YAAEC,SAASf;QAAqB;QAClD,GAAIL,aAAa;YACfqB,iBAAiB;gBACfC,QAAQ;oBACNC,UAAUtB,QAAQC,GAAG,CAACC,gBAAgB,IAAI;oBAC1CqB,cAAcvB,QAAQC,GAAG,CAACE,oBAAoB,IAAI;gBACpD;YACF;QACF,CAAC;QACDqB,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,KAAKlC,IAAI,CAAC,YAAY,CAAC;wBAC1C,MAAMsC,WAAW,GAAGvC,OAAOmC,KAAKlC,IAAI,EAAE,UAAU,CAAC;wBAEjD,qBAAqB;wBACrB,IAAIuC,OAAOD;wBACX,IAAIE,SAAS;wBACb,MAAO,KAAM;4BACX,MAAMC,WAAW,MAAMrC,OAAOP,YAAY,CAAC6C,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,MAAM1C,OAAOP,YAAY,CAACsC,MAAM,CAAC;4BAC/Ba,MAAM;gCACJC,IAAIL;gCACJ5C,MAAMqC;gCACNE;4BACF;wBACF;wBAEA,MAAMnC,OAAO8C,MAAM,CAACf,MAAM,CAAC;4BACzBa,MAAM;gCACJC,IAAIF;gCACJI,gBAAgBP;gCAChBQ,QAAQlB,KAAKe,EAAE;gCACfI,MAAM;4BACR;wBACF;oBACF;gBACF;YACF;QACF;QACAC,SAAS;YACPzD;YACAD,IAAI;gBACF2D,WAAWzD,uBAAuBc;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;
|
|
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
|
-
|
|
83
|
+
accessToken: bearerToken
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
if (!tokenRecord || !tokenRecord.userId) return null;
|
|
87
87
|
// Check expiration
|
|
88
|
-
if (
|
|
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: {
|
|
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":";;;;;;;;;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,aAAaJ;gBAAY;YACpC;YAEA,IAAI,CAACC,eAAe,CAACA,YAAYV,MAAM,EAAE,OAAO;YAEhD,mBAAmB;YACnB,IAAI,IAAIc,KAAKJ,YAAYK,oBAAoB,IAAI,IAAID,QAAQ;gBAC3D,OAAO;YACT;YAEA,eAAe;YACf,MAAMnB,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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
// 1. Try Bearer token (MCP clients like Claude Desktop, Cursor, etc.)
|
|
29
|
+
if (token) {
|
|
30
|
+
const user = await this.authService.validateMcpToken(token);
|
|
31
|
+
if (user) {
|
|
32
|
+
request.user = user;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
throw this.createUnauthorizedError('Invalid or expired MCP OAuth token');
|
|
36
|
+
}
|
|
37
|
+
// 2. Try session cookie (browser UI calling /info endpoints)
|
|
38
|
+
const session = await this.validateSessionCookie(request);
|
|
39
|
+
if (session) {
|
|
40
|
+
request.user = session;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// 3. Neither worked — require Bearer token (for MCP client discovery)
|
|
44
|
+
throw this.createUnauthorizedError('Bearer token required');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Attempt to validate session cookie from the request.
|
|
48
|
+
* Returns the user if a valid session exists, null otherwise.
|
|
49
|
+
*/ async validateSessionCookie(request) {
|
|
50
|
+
try {
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
for (const [key, value] of Object.entries(request.headers)){
|
|
53
|
+
if (value) {
|
|
54
|
+
headers.set(key, Array.isArray(value) ? value.join(', ') : value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const result = await this.authService.getSession(headers);
|
|
58
|
+
return result?.user ?? null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract Bearer token from Authorization header or access_token query param (SSE fallback).
|
|
65
|
+
*/ extractToken(request) {
|
|
66
|
+
const authHeader = request.headers.authorization;
|
|
67
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
68
|
+
return authHeader.slice(7);
|
|
69
|
+
}
|
|
70
|
+
// SSE connections may pass token as query parameter
|
|
71
|
+
const queryToken = request.query.access_token;
|
|
72
|
+
if (typeof queryToken === 'string' && queryToken.length > 0) {
|
|
73
|
+
return queryToken;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create UnauthorizedException with RFC 9728 WWW-Authenticate header
|
|
79
|
+
* pointing MCP clients to the protected resource metadata.
|
|
80
|
+
*/ createUnauthorizedError(message) {
|
|
81
|
+
const error = new UnauthorizedException(message);
|
|
82
|
+
error.wwwAuthenticate = createMcpWwwAuthenticateHeader(this.configService);
|
|
83
|
+
return error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
McpOAuthGuard = _ts_decorate([
|
|
87
|
+
Injectable(),
|
|
88
|
+
_ts_metadata("design:type", Function),
|
|
89
|
+
_ts_metadata("design:paramtypes", [
|
|
90
|
+
typeof AuthService === "undefined" ? Object : AuthService,
|
|
91
|
+
typeof ConfigService === "undefined" ? Object : ConfigService
|
|
92
|
+
])
|
|
93
|
+
], McpOAuthGuard);
|
|
94
|
+
|
|
95
|
+
//# 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 { type AuthUser, 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 // 1. Try Bearer token (MCP clients like Claude Desktop, Cursor, etc.)\n if (token) {\n const user = await this.authService.validateMcpToken(token);\n if (user) {\n request.user = user;\n return true;\n }\n throw this.createUnauthorizedError('Invalid or expired MCP OAuth token');\n }\n\n // 2. Try session cookie (browser UI calling /info endpoints)\n const session = await this.validateSessionCookie(request);\n if (session) {\n request.user = session;\n return true;\n }\n\n // 3. Neither worked — require Bearer token (for MCP client discovery)\n throw this.createUnauthorizedError('Bearer token required');\n }\n\n /**\n * Attempt to validate session cookie from the request.\n * Returns the user if a valid session exists, null otherwise.\n */\n private async validateSessionCookie(request: Request): Promise<AuthUser | null> {\n try {\n const headers = new Headers();\n for (const [key, value] of Object.entries(request.headers)) {\n if (value) {\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n }\n const result = await this.authService.getSession(headers);\n return result?.user ?? null;\n } catch {\n return null;\n }\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","user","validateMcpToken","createUnauthorizedError","session","validateSessionCookie","headers","Headers","key","value","Object","entries","set","Array","isArray","join","result","getSession","authHeader","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,SAAwBC,WAAW,QAAQ,oBAAoB;AAC/D,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,sEAAsE;QACtE,IAAIG,OAAO;YACT,MAAME,OAAO,MAAM,IAAI,CAACT,WAAW,CAACU,gBAAgB,CAACH;YACrD,IAAIE,MAAM;gBACRL,QAAQK,IAAI,GAAGA;gBACf,OAAO;YACT;YACA,MAAM,IAAI,CAACE,uBAAuB,CAAC;QACrC;QAEA,6DAA6D;QAC7D,MAAMC,UAAU,MAAM,IAAI,CAACC,qBAAqB,CAACT;QACjD,IAAIQ,SAAS;YACXR,QAAQK,IAAI,GAAGG;YACf,OAAO;QACT;QAEA,sEAAsE;QACtE,MAAM,IAAI,CAACD,uBAAuB,CAAC;IACrC;IAEA;;;GAGC,GACD,MAAcE,sBAAsBT,OAAgB,EAA4B;QAC9E,IAAI;YACF,MAAMU,UAAU,IAAIC;YACpB,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACf,QAAQU,OAAO,EAAG;gBAC1D,IAAIG,OAAO;oBACTH,QAAQM,GAAG,CAACJ,KAAKK,MAAMC,OAAO,CAACL,SAASA,MAAMM,IAAI,CAAC,QAAQN;gBAC7D;YACF;YACA,MAAMO,SAAS,MAAM,IAAI,CAACxB,WAAW,CAACyB,UAAU,CAACX;YACjD,OAAOU,QAAQf,QAAQ;QACzB,EAAE,OAAM;YACN,OAAO;QACT;IACF;IAEA;;GAEC,GACD,AAAQD,aAAaJ,OAAgB,EAAiB;QACpD,MAAMsB,aAAatB,QAAQU,OAAO,CAACa,aAAa;QAChD,IAAID,YAAYE,WAAW,YAAY;YACrC,OAAOF,WAAWG,KAAK,CAAC;QAC1B;QAEA,oDAAoD;QACpD,MAAMC,aAAa1B,QAAQ2B,KAAK,CAACC,YAAY;QAC7C,IAAI,OAAOF,eAAe,YAAYA,WAAWG,MAAM,GAAG,GAAG;YAC3D,OAAOH;QACT;QAEA,OAAO;IACT;IAEA;;;GAGC,GACD,AAAQnB,wBAAwBuB,OAAe,EAAyB;QACtE,MAAMC,QAAQ,IAAIxC,sBAAsBuC;QACxCC,MAAMC,eAAe,GAAGtC,+BAA+B,IAAI,CAACG,aAAa;QACzE,OAAOkC;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"}
|
|
@@ -31,7 +31,7 @@ export class HealthController {
|
|
|
31
31
|
* Returns which auth methods are available.
|
|
32
32
|
*/ getAuthConfig() {
|
|
33
33
|
return {
|
|
34
|
-
emailAndPassword:
|
|
34
|
+
emailAndPassword: process.env.AUTH_EMAIL_PASSWORD !== 'false',
|
|
35
35
|
google: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/health/health.controller.ts"],"sourcesContent":["/**\n * Health Controller\n *\n * Health check endpoints for monitoring and load balancers.\n */\n\nimport { Controller, Get } from '@nestjs/common';\nimport { Public } from '../auth/decorators/public.decorator.js';\nimport { PrismaService } from '../database/prisma.service.js';\n\n@Public()\n@Controller('health')\nexport class HealthController {\n constructor(private readonly prisma: PrismaService) {}\n\n /**\n * Basic liveness probe\n */\n @Get()\n async getHealth() {\n return {\n status: 'ok',\n timestamp: new Date().toISOString(),\n };\n }\n\n /**\n * Auth configuration for frontend feature detection.\n * Returns which auth methods are available.\n */\n @Get('auth-config')\n getAuthConfig() {\n return {\n emailAndPassword:
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/health/health.controller.ts"],"sourcesContent":["/**\n * Health Controller\n *\n * Health check endpoints for monitoring and load balancers.\n */\n\nimport { Controller, Get } from '@nestjs/common';\nimport { Public } from '../auth/decorators/public.decorator.js';\nimport { PrismaService } from '../database/prisma.service.js';\n\n@Public()\n@Controller('health')\nexport class HealthController {\n constructor(private readonly prisma: PrismaService) {}\n\n /**\n * Basic liveness probe\n */\n @Get()\n async getHealth() {\n return {\n status: 'ok',\n timestamp: new Date().toISOString(),\n };\n }\n\n /**\n * Auth configuration for frontend feature detection.\n * Returns which auth methods are available.\n */\n @Get('auth-config')\n getAuthConfig() {\n return {\n emailAndPassword: process.env.AUTH_EMAIL_PASSWORD !== 'false',\n google: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,\n };\n }\n\n /**\n * Readiness probe with database check\n */\n @Get('ready')\n async getReadiness() {\n try {\n // Test database connectivity\n await this.prisma.$queryRaw`SELECT 1`;\n\n return {\n status: 'ok',\n timestamp: new Date().toISOString(),\n database: 'connected',\n };\n } catch (error) {\n return {\n status: 'error',\n timestamp: new Date().toISOString(),\n database: 'disconnected',\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n}\n"],"names":["Controller","Get","Public","PrismaService","HealthController","prisma","getHealth","status","timestamp","Date","toISOString","getAuthConfig","emailAndPassword","process","env","AUTH_EMAIL_PASSWORD","google","GOOGLE_CLIENT_ID","GOOGLE_CLIENT_SECRET","getReadiness","$queryRaw","database","error","Error","message"],"mappings":";;;;;;;;;AAAA;;;;CAIC,GAED,SAASA,UAAU,EAAEC,GAAG,QAAQ,iBAAiB;AACjD,SAASC,MAAM,QAAQ,yCAAyC;AAChE,SAASC,aAAa,QAAQ,gCAAgC;AAI9D,OAAO,MAAMC;IACX,YAAY,AAAiBC,MAAqB,CAAE;aAAvBA,SAAAA;IAAwB;IAErD;;GAEC,GACD,MACMC,YAAY;QAChB,OAAO;YACLC,QAAQ;YACRC,WAAW,IAAIC,OAAOC,WAAW;QACnC;IACF;IAEA;;;GAGC,GACD,AACAC,gBAAgB;QACd,OAAO;YACLC,kBAAkBC,QAAQC,GAAG,CAACC,mBAAmB,KAAK;YACtDC,QAAQ,CAAC,CAACH,QAAQC,GAAG,CAACG,gBAAgB,IAAI,CAAC,CAACJ,QAAQC,GAAG,CAACI,oBAAoB;QAC9E;IACF;IAEA;;GAEC,GACD,MACMC,eAAe;QACnB,IAAI;YACF,6BAA6B;YAC7B,MAAM,IAAI,CAACd,MAAM,CAACe,SAAS,CAAC,QAAQ,CAAC;YAErC,OAAO;gBACLb,QAAQ;gBACRC,WAAW,IAAIC,OAAOC,WAAW;gBACjCW,UAAU;YACZ;QACF,EAAE,OAAOC,OAAO;YACd,OAAO;gBACLf,QAAQ;gBACRC,WAAW,IAAIC,OAAOC,WAAW;gBACjCW,UAAU;gBACVC,OAAOA,iBAAiBC,QAAQD,MAAME,OAAO,GAAG;YAClD;QACF;IACF;AACF"}
|