@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
@@ -1,9 +1,9 @@
1
1
 
2
- > @dxheroes/local-mcp-backend@0.9.2 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
2
+ > @dxheroes/local-mcp-backend@0.10.0 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
3
3
  > nest build
4
4
 
5
5
  -  TSC  Initializing type checker...
6
6
  ✔  TSC  Initializing type checker...
7
7
  >  TSC  Found 0 issues.
8
8
  >  SWC  Running...
9
- Successfully compiled: 56 files with swc (92.29ms)
9
+ Successfully compiled: 60 files with swc (100.08ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.9.2...backend-v0.10.0) (2026-03-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * **auth:** implement MCP OAuth guard and enhance authentication flow ([14f6355](https://github.com/DXHeroes/local-mcp-gateway/commit/14f6355b7e3e1d2a2a757643c7bc1946aa1e09cd))
9
+ * enhance OAuth integration and add custom headers support for MCP servers ([0904743](https://github.com/DXHeroes/local-mcp-gateway/commit/09047436072810f5876a2c92ba47f6f90f8f6103))
10
+
11
+
12
+ ### Code Refactoring
13
+
14
+ * **auth:** streamline MCP OAuth guard and enhance configuration ([38972a3](https://github.com/DXHeroes/local-mcp-gateway/commit/38972a3c94c377d8f5d6c9c714d6ccab91765a99))
15
+ * **database:** update OAuth models and migrations for improved schema ([a92b074](https://github.com/DXHeroes/local-mcp-gateway/commit/a92b07400050b30decdda1dc82f4647d49c38c91))
16
+
17
+
18
+ ### Dependencies
19
+
20
+ * The following workspace dependencies were updated
21
+ * dependencies
22
+ * @dxheroes/local-mcp-core bumped to 0.8.0
23
+ * @dxheroes/local-mcp-database bumped to 0.5.3
24
+ * @dxheroes/mcp-gemini-deep-research bumped to 0.5.6
25
+ * @dxheroes/mcp-merk bumped to 0.3.6
26
+ * @dxheroes/mcp-toggl bumped to 0.3.6
27
+ * devDependencies
28
+ * @dxheroes/local-mcp-config bumped to 0.4.11
29
+
3
30
  ## [0.9.2](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.9.1...backend-v0.9.2) (2026-03-11)
4
31
 
5
32
 
@@ -0,0 +1,246 @@
1
+ import { createServer } from "node:http";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AllExceptionsFilter } from "../../common/filters/all-exceptions.filter.js";
4
+ import { McpOAuthGuard } from "../../modules/auth/mcp-oauth.guard.js";
5
+ import { createMcpProtectedResourceMetadata, resolvePublicAuthBaseUrl, resolvePublicBackendOrigin } from "../../modules/auth/mcp-oauth.utils.js";
6
+ function createConfigService(values) {
7
+ return {
8
+ get: vi.fn((key)=>values[key])
9
+ };
10
+ }
11
+ function createAuthServiceMock() {
12
+ return {
13
+ validateMcpToken: vi.fn().mockResolvedValue(null)
14
+ };
15
+ }
16
+ async function startTestApp(options) {
17
+ const authService = createAuthServiceMock();
18
+ const configService = createConfigService({
19
+ 'app.port': 3001,
20
+ BETTER_AUTH_URL: options?.backendUrl ?? 'http://localhost:3001'
21
+ });
22
+ const guard = new McpOAuthGuard(authService, configService);
23
+ const filter = new AllExceptionsFilter();
24
+ const backendOrigin = resolvePublicBackendOrigin(configService);
25
+ const authBaseUrl = resolvePublicAuthBaseUrl(configService);
26
+ const createRequest = (req)=>{
27
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
28
+ return {
29
+ method: req.method ?? 'GET',
30
+ url: `${url.pathname}${url.search}`,
31
+ headers: req.headers,
32
+ query: Object.fromEntries(url.searchParams.entries()),
33
+ user: undefined
34
+ };
35
+ };
36
+ const createResponse = (res)=>{
37
+ const response = {
38
+ setHeader (name, value) {
39
+ res.setHeader(name, value);
40
+ return response;
41
+ },
42
+ status (statusCode) {
43
+ res.statusCode = statusCode;
44
+ return response;
45
+ },
46
+ json (body) {
47
+ res.setHeader('content-type', 'application/json');
48
+ res.end(JSON.stringify(body));
49
+ }
50
+ };
51
+ return response;
52
+ };
53
+ const handleGuardedRoute = async (nodeRequest, nodeResponse, responseBody)=>{
54
+ const request = createRequest(nodeRequest);
55
+ const response = createResponse(nodeResponse);
56
+ const context = {
57
+ switchToHttp: ()=>({
58
+ getRequest: ()=>request,
59
+ getResponse: ()=>response
60
+ })
61
+ };
62
+ const host = {
63
+ switchToHttp: ()=>({
64
+ getRequest: ()=>request,
65
+ getResponse: ()=>response
66
+ })
67
+ };
68
+ try {
69
+ await guard.canActivate(context);
70
+ response.json(responseBody(request));
71
+ } catch (error) {
72
+ filter.catch(error, host);
73
+ }
74
+ };
75
+ const server = await new Promise((resolve)=>{
76
+ const listeningServer = createServer(async (req, res)=>{
77
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
78
+ if (req.method === 'GET' && url.pathname === '/api/mcp/test') {
79
+ await handleGuardedRoute(req, res, (request)=>({
80
+ ok: true,
81
+ userId: request.user?.id ?? null
82
+ }));
83
+ return;
84
+ }
85
+ if (req.method === 'GET' && url.pathname === '/api/mcp/sse') {
86
+ await handleGuardedRoute(req, res, (request)=>({
87
+ ok: true,
88
+ token: request.query.access_token ?? null,
89
+ userId: request.user?.id ?? null
90
+ }));
91
+ return;
92
+ }
93
+ if (req.method === 'GET' && url.pathname === '/.well-known/oauth-protected-resource') {
94
+ res.setHeader('content-type', 'application/json');
95
+ res.end(JSON.stringify(createMcpProtectedResourceMetadata(configService)));
96
+ return;
97
+ }
98
+ if (req.method === 'GET' && url.pathname === '/.well-known/oauth-authorization-server') {
99
+ res.setHeader('content-type', 'application/json');
100
+ res.end(JSON.stringify({
101
+ issuer: backendOrigin,
102
+ authorization_endpoint: `${authBaseUrl}/mcp/authorize`,
103
+ token_endpoint: `${authBaseUrl}/mcp/token`,
104
+ registration_endpoint: `${authBaseUrl}/mcp/register`,
105
+ jwks_uri: `${authBaseUrl}/mcp/jwks`,
106
+ response_types_supported: [
107
+ 'code'
108
+ ],
109
+ grant_types_supported: [
110
+ 'authorization_code',
111
+ 'refresh_token'
112
+ ],
113
+ code_challenge_methods_supported: [
114
+ 'S256'
115
+ ]
116
+ }));
117
+ return;
118
+ }
119
+ if (req.method === 'POST' && url.pathname === '/api/auth/mcp/register') {
120
+ res.statusCode = 201;
121
+ res.setHeader('content-type', 'application/json');
122
+ res.end(JSON.stringify({
123
+ client_id: 'cursor-client',
124
+ client_secret: 'cursor-secret',
125
+ redirect_uris: [
126
+ 'https://cursor.sh/callback'
127
+ ]
128
+ }));
129
+ return;
130
+ }
131
+ res.statusCode = 404;
132
+ res.end();
133
+ }).listen(0, ()=>resolve(listeningServer));
134
+ });
135
+ const address = server.address();
136
+ if (!address || typeof address === 'string') {
137
+ throw new Error('Failed to resolve test server address');
138
+ }
139
+ return {
140
+ close: ()=>new Promise((resolve, reject)=>{
141
+ server.close((error)=>{
142
+ if (error) {
143
+ reject(error);
144
+ return;
145
+ }
146
+ resolve();
147
+ });
148
+ }),
149
+ authService,
150
+ baseUrl: `http://127.0.0.1:${address.port}`
151
+ };
152
+ }
153
+ describe('MCP proxy auth HTTP contract', ()=>{
154
+ let close;
155
+ let authService;
156
+ let baseUrl;
157
+ beforeEach(async ()=>{
158
+ const result = await startTestApp({
159
+ backendUrl: 'http://localhost:9631'
160
+ });
161
+ close = result.close;
162
+ authService = result.authService;
163
+ baseUrl = result.baseUrl;
164
+ });
165
+ afterEach(async ()=>{
166
+ if (close) {
167
+ await close();
168
+ }
169
+ });
170
+ it('returns 401 with MCP discovery headers when token is missing', async ()=>{
171
+ const response = await fetch(`${baseUrl}/api/mcp/test`);
172
+ const body = await response.json();
173
+ expect(response.status).toBe(401);
174
+ expect(response.headers.get('access-control-expose-headers')).toBe('WWW-Authenticate');
175
+ expect(response.headers.get('www-authenticate')).toContain('resource_metadata=');
176
+ expect(response.headers.get('www-authenticate')).toContain('resource_metadata_uri=');
177
+ expect(body.message).toBe('Bearer token required');
178
+ });
179
+ it('serves protected resource metadata with Better Auth-backed URLs', async ()=>{
180
+ const response = await fetch(`${baseUrl}/.well-known/oauth-protected-resource`);
181
+ const body = await response.json();
182
+ expect(response.status).toBe(200);
183
+ expect(body).toEqual({
184
+ resource: 'http://localhost:9631/api/mcp',
185
+ authorization_servers: [
186
+ 'http://localhost:9631'
187
+ ],
188
+ bearer_methods_supported: [
189
+ 'header'
190
+ ],
191
+ scopes_supported: [
192
+ 'openid',
193
+ 'profile',
194
+ 'email',
195
+ 'offline_access'
196
+ ],
197
+ jwks_uri: 'http://localhost:9631/api/auth/mcp/jwks',
198
+ resource_signing_alg_values_supported: [
199
+ 'RS256',
200
+ 'none'
201
+ ]
202
+ });
203
+ });
204
+ it('serves authorization server metadata with MCP registration endpoint', async ()=>{
205
+ const response = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`);
206
+ const body = await response.json();
207
+ expect(response.status).toBe(200);
208
+ expect(body.registration_endpoint).toBe('http://localhost:9631/api/auth/mcp/register');
209
+ expect(body.authorization_endpoint).toBe('http://localhost:9631/api/auth/mcp/authorize');
210
+ expect(body.token_endpoint).toBe('http://localhost:9631/api/auth/mcp/token');
211
+ });
212
+ it('exposes the advertised registration endpoint', async ()=>{
213
+ const response = await fetch(`${baseUrl}/api/auth/mcp/register`, {
214
+ method: 'POST',
215
+ headers: {
216
+ 'content-type': 'application/json'
217
+ },
218
+ body: JSON.stringify({
219
+ redirect_uris: [
220
+ 'https://cursor.sh/callback'
221
+ ]
222
+ })
223
+ });
224
+ const body = await response.json();
225
+ expect(response.status).toBe(201);
226
+ expect(body.client_id).toBe('cursor-client');
227
+ });
228
+ it('accepts access_token query params for SSE fallback', async ()=>{
229
+ authService.validateMcpToken.mockResolvedValue({
230
+ id: 'user-sse',
231
+ name: 'SSE User',
232
+ email: 'sse@example.com'
233
+ });
234
+ const response = await fetch(`${baseUrl}/api/mcp/sse?access_token=sse-token`);
235
+ const body = await response.json();
236
+ expect(response.status).toBe(200);
237
+ expect(body).toEqual({
238
+ ok: true,
239
+ token: 'sse-token',
240
+ userId: 'user-sse'
241
+ });
242
+ expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');
243
+ });
244
+ });
245
+
246
+ //# sourceMappingURL=mcp-proxy-auth-http.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/integration/mcp-proxy-auth-http.test.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport type { ConfigService } from '@nestjs/config';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AllExceptionsFilter } from '../../common/filters/all-exceptions.filter.js';\nimport { AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\nimport { McpOAuthGuard } from '../../modules/auth/mcp-oauth.guard.js';\nimport {\n createMcpProtectedResourceMetadata,\n resolvePublicAuthBaseUrl,\n resolvePublicBackendOrigin,\n} from '../../modules/auth/mcp-oauth.utils.js';\n\ntype MockAuthService = {\n validateMcpToken: ReturnType<typeof vi.fn>;\n};\n\ntype MockConfigService = {\n get: ReturnType<typeof vi.fn>;\n};\n\nfunction createConfigService(values: Record<string, unknown>): MockConfigService {\n return {\n get: vi.fn((key: string) => values[key]),\n };\n}\n\nfunction createAuthServiceMock(): MockAuthService {\n return {\n validateMcpToken: vi.fn().mockResolvedValue(null),\n };\n}\n\nasync function startTestApp(options?: {\n backendUrl?: string;\n}): Promise<{\n close: () => Promise<void>;\n authService: MockAuthService;\n baseUrl: string;\n}> {\n const authService = createAuthServiceMock();\n const configService = createConfigService({\n 'app.port': 3001,\n BETTER_AUTH_URL: options?.backendUrl ?? 'http://localhost:3001',\n });\n const guard = new McpOAuthGuard(authService as unknown as AuthService, configService as never);\n const filter = new AllExceptionsFilter();\n const backendOrigin = resolvePublicBackendOrigin(configService as unknown as ConfigService);\n const authBaseUrl = resolvePublicAuthBaseUrl(configService as unknown as ConfigService);\n\n const createRequest = (req: IncomingMessage) => {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);\n return {\n method: req.method ?? 'GET',\n url: `${url.pathname}${url.search}`,\n headers: req.headers,\n query: Object.fromEntries(url.searchParams.entries()),\n user: undefined,\n };\n };\n\n const createResponse = (res: ServerResponse) => {\n const response = {\n setHeader(name: string, value: string) {\n res.setHeader(name, value);\n return response;\n },\n status(statusCode: number) {\n res.statusCode = statusCode;\n return response;\n },\n json(body: unknown) {\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(body));\n },\n };\n\n return response;\n };\n\n const handleGuardedRoute = async (\n nodeRequest: IncomingMessage,\n nodeResponse: ServerResponse,\n responseBody: (request: ReturnType<typeof createRequest>) => Record<string, unknown>\n ) => {\n const request = createRequest(nodeRequest);\n const response = createResponse(nodeResponse);\n const context = {\n switchToHttp: () => ({\n getRequest: () => request,\n getResponse: () => response,\n }),\n } as never;\n\n const host = {\n switchToHttp: () => ({\n getRequest: () => request,\n getResponse: () => response,\n }),\n } as never;\n\n try {\n await guard.canActivate(context);\n response.json(responseBody(request));\n } catch (error) {\n filter.catch(error, host);\n }\n };\n\n const server = await new Promise<import('node:http').Server>((resolve) => {\n const listeningServer = createServer(async (req, res) => {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);\n\n if (req.method === 'GET' && url.pathname === '/api/mcp/test') {\n await handleGuardedRoute(req, res, (request) => ({\n ok: true,\n userId: (request as { user?: AuthUser }).user?.id ?? null,\n }));\n return;\n }\n\n if (req.method === 'GET' && url.pathname === '/api/mcp/sse') {\n await handleGuardedRoute(req, res, (request) => ({\n ok: true,\n token: request.query.access_token ?? null,\n userId: (request as { user?: AuthUser }).user?.id ?? null,\n }));\n return;\n }\n\n if (req.method === 'GET' && url.pathname === '/.well-known/oauth-protected-resource') {\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(createMcpProtectedResourceMetadata(configService as unknown as ConfigService)));\n return;\n }\n\n if (req.method === 'GET' && url.pathname === '/.well-known/oauth-authorization-server') {\n res.setHeader('content-type', 'application/json');\n res.end(\n JSON.stringify({\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 code_challenge_methods_supported: ['S256'],\n })\n );\n return;\n }\n\n if (req.method === 'POST' && url.pathname === '/api/auth/mcp/register') {\n res.statusCode = 201;\n res.setHeader('content-type', 'application/json');\n res.end(\n JSON.stringify({\n client_id: 'cursor-client',\n client_secret: 'cursor-secret',\n redirect_uris: ['https://cursor.sh/callback'],\n })\n );\n return;\n }\n\n res.statusCode = 404;\n res.end();\n }).listen(0, () => resolve(listeningServer));\n });\n const address = server.address();\n if (!address || typeof address === 'string') {\n throw new Error('Failed to resolve test server address');\n }\n\n return {\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n }),\n authService,\n baseUrl: `http://127.0.0.1:${address.port}`,\n };\n}\n\ndescribe('MCP proxy auth HTTP contract', () => {\n let close: (() => Promise<void>) | undefined;\n let authService: MockAuthService;\n let baseUrl: string;\n\n beforeEach(async () => {\n const result = await startTestApp({\n backendUrl: 'http://localhost:9631',\n });\n close = result.close;\n authService = result.authService;\n baseUrl = result.baseUrl;\n });\n\n afterEach(async () => {\n if (close) {\n await close();\n }\n });\n\n it('returns 401 with MCP discovery headers when token is missing', async () => {\n const response = await fetch(`${baseUrl}/api/mcp/test`);\n const body = (await response.json()) as Record<string, unknown>;\n\n expect(response.status).toBe(401);\n expect(response.headers.get('access-control-expose-headers')).toBe('WWW-Authenticate');\n expect(response.headers.get('www-authenticate')).toContain('resource_metadata=');\n expect(response.headers.get('www-authenticate')).toContain('resource_metadata_uri=');\n expect(body.message).toBe('Bearer token required');\n });\n\n it('serves protected resource metadata with Better Auth-backed URLs', async () => {\n const response = await fetch(`${baseUrl}/.well-known/oauth-protected-resource`);\n const body = (await response.json()) as Record<string, unknown>;\n\n expect(response.status).toBe(200);\n expect(body).toEqual({\n resource: 'http://localhost:9631/api/mcp',\n authorization_servers: ['http://localhost:9631'],\n bearer_methods_supported: ['header'],\n scopes_supported: ['openid', 'profile', 'email', 'offline_access'],\n jwks_uri: 'http://localhost:9631/api/auth/mcp/jwks',\n resource_signing_alg_values_supported: ['RS256', 'none'],\n });\n });\n\n it('serves authorization server metadata with MCP registration endpoint', async () => {\n const response = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`);\n const body = (await response.json()) as Record<string, unknown>;\n\n expect(response.status).toBe(200);\n expect(body.registration_endpoint).toBe('http://localhost:9631/api/auth/mcp/register');\n expect(body.authorization_endpoint).toBe('http://localhost:9631/api/auth/mcp/authorize');\n expect(body.token_endpoint).toBe('http://localhost:9631/api/auth/mcp/token');\n });\n\n it('exposes the advertised registration endpoint', async () => {\n const response = await fetch(`${baseUrl}/api/auth/mcp/register`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n redirect_uris: ['https://cursor.sh/callback'],\n }),\n });\n const body = (await response.json()) as Record<string, unknown>;\n\n expect(response.status).toBe(201);\n expect(body.client_id).toBe('cursor-client');\n });\n\n it('accepts access_token query params for SSE fallback', async () => {\n authService.validateMcpToken.mockResolvedValue({\n id: 'user-sse',\n name: 'SSE User',\n email: 'sse@example.com',\n });\n\n const response = await fetch(`${baseUrl}/api/mcp/sse?access_token=sse-token`);\n const body = (await response.json()) as Record<string, unknown>;\n\n expect(response.status).toBe(200);\n expect(body).toEqual({\n ok: true,\n token: 'sse-token',\n userId: 'user-sse',\n });\n expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');\n });\n});\n"],"names":["createServer","afterEach","beforeEach","describe","expect","it","vi","AllExceptionsFilter","McpOAuthGuard","createMcpProtectedResourceMetadata","resolvePublicAuthBaseUrl","resolvePublicBackendOrigin","createConfigService","values","get","fn","key","createAuthServiceMock","validateMcpToken","mockResolvedValue","startTestApp","options","authService","configService","BETTER_AUTH_URL","backendUrl","guard","filter","backendOrigin","authBaseUrl","createRequest","req","url","URL","headers","host","method","pathname","search","query","Object","fromEntries","searchParams","entries","user","undefined","createResponse","res","response","setHeader","name","value","status","statusCode","json","body","end","JSON","stringify","handleGuardedRoute","nodeRequest","nodeResponse","responseBody","request","context","switchToHttp","getRequest","getResponse","canActivate","error","catch","server","Promise","resolve","listeningServer","ok","userId","id","token","access_token","issuer","authorization_endpoint","token_endpoint","registration_endpoint","jwks_uri","response_types_supported","grant_types_supported","code_challenge_methods_supported","client_id","client_secret","redirect_uris","listen","address","Error","close","reject","baseUrl","port","result","fetch","toBe","toContain","message","toEqual","resource","authorization_servers","bearer_methods_supported","scopes_supported","resource_signing_alg_values_supported","email","toHaveBeenCalledWith"],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { createServer } from "node:http";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { OAuthController } from "../../modules/oauth/oauth.controller.js";
4
+ function createOAuthServiceMock() {
5
+ return {
6
+ discoverAndAuthorize: vi.fn(),
7
+ handleCallback: vi.fn()
8
+ };
9
+ }
10
+ describe('OAuth authorize and callback HTTP contract', ()=>{
11
+ let close;
12
+ let oauthService;
13
+ let baseUrl;
14
+ beforeEach(async ()=>{
15
+ oauthService = createOAuthServiceMock();
16
+ const controller = new OAuthController(oauthService);
17
+ const server = await new Promise((resolve)=>{
18
+ const listeningServer = createServer(async (req, res)=>{
19
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
20
+ const request = {
21
+ protocol: 'http',
22
+ query: Object.fromEntries(url.searchParams.entries()),
23
+ get: (headerName)=>{
24
+ if (headerName.toLowerCase() === 'host') {
25
+ return req.headers.host ?? '';
26
+ }
27
+ return undefined;
28
+ }
29
+ };
30
+ const response = {
31
+ redirect (location) {
32
+ res.statusCode = 302;
33
+ res.setHeader('location', location);
34
+ res.end();
35
+ },
36
+ status (statusCode) {
37
+ res.statusCode = statusCode;
38
+ return response;
39
+ },
40
+ send (body) {
41
+ res.setHeader('content-type', 'text/html; charset=utf-8');
42
+ res.end(body);
43
+ }
44
+ };
45
+ if (req.method === 'GET' && url.pathname.startsWith('/api/oauth/authorize/')) {
46
+ const serverId = url.pathname.split('/').pop() ?? '';
47
+ await controller.authorize(serverId, request, response);
48
+ return;
49
+ }
50
+ if (req.method === 'GET' && url.pathname === '/api/oauth/callback') {
51
+ await controller.callback(typeof request.query.code === 'string' ? request.query.code : '', typeof request.query.state === 'string' ? request.query.state : '', request, response);
52
+ return;
53
+ }
54
+ res.statusCode = 404;
55
+ res.end();
56
+ }).listen(0, ()=>resolve(listeningServer));
57
+ });
58
+ const address = server.address();
59
+ if (!address || typeof address === 'string') {
60
+ throw new Error('Failed to resolve test server address');
61
+ }
62
+ close = ()=>new Promise((resolve, reject)=>{
63
+ server.close((error)=>{
64
+ if (error) {
65
+ reject(error);
66
+ return;
67
+ }
68
+ resolve();
69
+ });
70
+ });
71
+ baseUrl = `http://127.0.0.1:${address.port}`;
72
+ });
73
+ afterEach(async ()=>{
74
+ if (close) {
75
+ await close();
76
+ }
77
+ });
78
+ it('redirects authorize requests to the discovered authorization URL', async ()=>{
79
+ oauthService.discoverAndAuthorize.mockResolvedValue('https://auth.example.com/authorize?client_id=abc');
80
+ const response = await fetch(`${baseUrl}/api/oauth/authorize/server-1`, {
81
+ redirect: 'manual'
82
+ });
83
+ expect(response.status).toBe(302);
84
+ expect(response.headers.get('location')).toBe('https://auth.example.com/authorize?client_id=abc');
85
+ expect(oauthService.discoverAndAuthorize).toHaveBeenCalledWith('server-1', `${baseUrl}/api/oauth/callback`);
86
+ });
87
+ it('renders an error page when authorize discovery fails', async ()=>{
88
+ oauthService.discoverAndAuthorize.mockRejectedValue(new Error('Metadata discovery failed'));
89
+ const response = await fetch(`${baseUrl}/api/oauth/authorize/server-1`);
90
+ const html = await response.text();
91
+ expect(response.status).toBe(400);
92
+ expect(html).toContain('Error: Metadata discovery failed');
93
+ });
94
+ it('rejects callbacks without code or state', async ()=>{
95
+ const response = await fetch(`${baseUrl}/api/oauth/callback`);
96
+ const html = await response.text();
97
+ expect(response.status).toBe(400);
98
+ expect(html).toContain('Missing code or state parameter');
99
+ });
100
+ it('renders a success page for valid callbacks', async ()=>{
101
+ oauthService.handleCallback.mockResolvedValue({
102
+ success: true
103
+ });
104
+ const response = await fetch(`${baseUrl}/api/oauth/callback?code=auth-code&state=server-1`);
105
+ const html = await response.text();
106
+ expect(response.status).toBe(200);
107
+ expect(html).toContain('Authorization successful! This window will close.');
108
+ expect(oauthService.handleCallback).toHaveBeenCalledWith('server-1', 'auth-code', `${baseUrl}/api/oauth/callback`);
109
+ });
110
+ it('renders a failure page when callback token exchange fails', async ()=>{
111
+ oauthService.handleCallback.mockResolvedValue({
112
+ success: false,
113
+ error: 'Token exchange failed'
114
+ });
115
+ const response = await fetch(`${baseUrl}/api/oauth/callback?code=auth-code&state=server-1`);
116
+ const html = await response.text();
117
+ expect(response.status).toBe(200);
118
+ expect(html).toContain('Error: Token exchange failed');
119
+ });
120
+ });
121
+
122
+ //# sourceMappingURL=oauth-authorize-callback.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/integration/oauth-authorize-callback.test.ts"],"sourcesContent":["import { createServer } from 'node:http';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { OAuthController } from '../../modules/oauth/oauth.controller.js';\nimport { OAuthService } from '../../modules/oauth/oauth.service.js';\n\nfunction createOAuthServiceMock() {\n return {\n discoverAndAuthorize: vi.fn(),\n handleCallback: vi.fn(),\n };\n}\n\ndescribe('OAuth authorize and callback HTTP contract', () => {\n let close: (() => Promise<void>) | undefined;\n let oauthService: ReturnType<typeof createOAuthServiceMock>;\n let baseUrl: string;\n\n beforeEach(async () => {\n oauthService = createOAuthServiceMock();\n const controller = new OAuthController(oauthService as unknown as OAuthService);\n\n const server = await new Promise<import('node:http').Server>((resolve) => {\n const listeningServer = createServer(async (req, res) => {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);\n const request = {\n protocol: 'http',\n query: Object.fromEntries(url.searchParams.entries()),\n get: (headerName: string) => {\n if (headerName.toLowerCase() === 'host') {\n return req.headers.host ?? '';\n }\n return undefined;\n },\n };\n const response = {\n redirect(location: string) {\n res.statusCode = 302;\n res.setHeader('location', location);\n res.end();\n },\n status(statusCode: number) {\n res.statusCode = statusCode;\n return response;\n },\n send(body: string) {\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(body);\n },\n };\n\n if (req.method === 'GET' && url.pathname.startsWith('/api/oauth/authorize/')) {\n const serverId = url.pathname.split('/').pop() ?? '';\n await controller.authorize(serverId, request as never, response as never);\n return;\n }\n\n if (req.method === 'GET' && url.pathname === '/api/oauth/callback') {\n await controller.callback(\n typeof request.query.code === 'string' ? request.query.code : '',\n typeof request.query.state === 'string' ? request.query.state : '',\n request as never,\n response as never\n );\n return;\n }\n\n res.statusCode = 404;\n res.end();\n }).listen(0, () => resolve(listeningServer));\n });\n const address = server.address();\n if (!address || typeof address === 'string') {\n throw new Error('Failed to resolve test server address');\n }\n\n close = () =>\n new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n });\n baseUrl = `http://127.0.0.1:${address.port}`;\n });\n\n afterEach(async () => {\n if (close) {\n await close();\n }\n });\n\n it('redirects authorize requests to the discovered authorization URL', async () => {\n oauthService.discoverAndAuthorize.mockResolvedValue('https://auth.example.com/authorize?client_id=abc');\n\n const response = await fetch(`${baseUrl}/api/oauth/authorize/server-1`, {\n redirect: 'manual',\n });\n\n expect(response.status).toBe(302);\n expect(response.headers.get('location')).toBe('https://auth.example.com/authorize?client_id=abc');\n expect(oauthService.discoverAndAuthorize).toHaveBeenCalledWith(\n 'server-1',\n `${baseUrl}/api/oauth/callback`\n );\n });\n\n it('renders an error page when authorize discovery fails', async () => {\n oauthService.discoverAndAuthorize.mockRejectedValue(new Error('Metadata discovery failed'));\n\n const response = await fetch(`${baseUrl}/api/oauth/authorize/server-1`);\n const html = await response.text();\n\n expect(response.status).toBe(400);\n expect(html).toContain('Error: Metadata discovery failed');\n });\n\n it('rejects callbacks without code or state', async () => {\n const response = await fetch(`${baseUrl}/api/oauth/callback`);\n const html = await response.text();\n\n expect(response.status).toBe(400);\n expect(html).toContain('Missing code or state parameter');\n });\n\n it('renders a success page for valid callbacks', async () => {\n oauthService.handleCallback.mockResolvedValue({ success: true });\n\n const response = await fetch(`${baseUrl}/api/oauth/callback?code=auth-code&state=server-1`);\n const html = await response.text();\n\n expect(response.status).toBe(200);\n expect(html).toContain('Authorization successful! This window will close.');\n expect(oauthService.handleCallback).toHaveBeenCalledWith(\n 'server-1',\n 'auth-code',\n `${baseUrl}/api/oauth/callback`\n );\n });\n\n it('renders a failure page when callback token exchange fails', async () => {\n oauthService.handleCallback.mockResolvedValue({\n success: false,\n error: 'Token exchange failed',\n });\n\n const response = await fetch(`${baseUrl}/api/oauth/callback?code=auth-code&state=server-1`);\n const html = await response.text();\n\n expect(response.status).toBe(200);\n expect(html).toContain('Error: Token exchange failed');\n });\n});\n"],"names":["createServer","afterEach","beforeEach","describe","expect","it","vi","OAuthController","createOAuthServiceMock","discoverAndAuthorize","fn","handleCallback","close","oauthService","baseUrl","controller","server","Promise","resolve","listeningServer","req","res","url","URL","headers","host","request","protocol","query","Object","fromEntries","searchParams","entries","get","headerName","toLowerCase","undefined","response","redirect","location","statusCode","setHeader","end","status","send","body","method","pathname","startsWith","serverId","split","pop","authorize","callback","code","state","listen","address","Error","reject","error","port","mockResolvedValue","fetch","toBe","toHaveBeenCalledWith","mockRejectedValue","html","text","toContain","success"],"mappings":"AAAA,SAASA,YAAY,QAAQ,YAAY;AACzC,SAASC,SAAS,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AACzE,SAASC,eAAe,QAAQ,0CAA0C;AAG1E,SAASC;IACP,OAAO;QACLC,sBAAsBH,GAAGI,EAAE;QAC3BC,gBAAgBL,GAAGI,EAAE;IACvB;AACF;AAEAP,SAAS,8CAA8C;IACrD,IAAIS;IACJ,IAAIC;IACJ,IAAIC;IAEJZ,WAAW;QACTW,eAAeL;QACf,MAAMO,aAAa,IAAIR,gBAAgBM;QAEvC,MAAMG,SAAS,MAAM,IAAIC,QAAoC,CAACC;YAC5D,MAAMC,kBAAkBnB,aAAa,OAAOoB,KAAKC;gBAC/C,MAAMC,MAAM,IAAIC,IAAIH,IAAIE,GAAG,IAAI,KAAK,CAAC,OAAO,EAAEF,IAAII,OAAO,CAACC,IAAI,IAAI,aAAa;gBAC/E,MAAMC,UAAU;oBACdC,UAAU;oBACVC,OAAOC,OAAOC,WAAW,CAACR,IAAIS,YAAY,CAACC,OAAO;oBAClDC,KAAK,CAACC;wBACJ,IAAIA,WAAWC,WAAW,OAAO,QAAQ;4BACvC,OAAOf,IAAII,OAAO,CAACC,IAAI,IAAI;wBAC7B;wBACA,OAAOW;oBACT;gBACF;gBACA,MAAMC,WAAW;oBACfC,UAASC,QAAgB;wBACvBlB,IAAImB,UAAU,GAAG;wBACjBnB,IAAIoB,SAAS,CAAC,YAAYF;wBAC1BlB,IAAIqB,GAAG;oBACT;oBACAC,QAAOH,UAAkB;wBACvBnB,IAAImB,UAAU,GAAGA;wBACjB,OAAOH;oBACT;oBACAO,MAAKC,IAAY;wBACfxB,IAAIoB,SAAS,CAAC,gBAAgB;wBAC9BpB,IAAIqB,GAAG,CAACG;oBACV;gBACF;gBAEA,IAAIzB,IAAI0B,MAAM,KAAK,SAASxB,IAAIyB,QAAQ,CAACC,UAAU,CAAC,0BAA0B;oBAC5E,MAAMC,WAAW3B,IAAIyB,QAAQ,CAACG,KAAK,CAAC,KAAKC,GAAG,MAAM;oBAClD,MAAMpC,WAAWqC,SAAS,CAACH,UAAUvB,SAAkBW;oBACvD;gBACF;gBAEA,IAAIjB,IAAI0B,MAAM,KAAK,SAASxB,IAAIyB,QAAQ,KAAK,uBAAuB;oBAClE,MAAMhC,WAAWsC,QAAQ,CACvB,OAAO3B,QAAQE,KAAK,CAAC0B,IAAI,KAAK,WAAW5B,QAAQE,KAAK,CAAC0B,IAAI,GAAG,IAC9D,OAAO5B,QAAQE,KAAK,CAAC2B,KAAK,KAAK,WAAW7B,QAAQE,KAAK,CAAC2B,KAAK,GAAG,IAChE7B,SACAW;oBAEF;gBACF;gBAEAhB,IAAImB,UAAU,GAAG;gBACjBnB,IAAIqB,GAAG;YACT,GAAGc,MAAM,CAAC,GAAG,IAAMtC,QAAQC;QAC7B;QACA,MAAMsC,UAAUzC,OAAOyC,OAAO;QAC9B,IAAI,CAACA,WAAW,OAAOA,YAAY,UAAU;YAC3C,MAAM,IAAIC,MAAM;QAClB;QAEA9C,QAAQ,IACN,IAAIK,QAAc,CAACC,SAASyC;gBAC1B3C,OAAOJ,KAAK,CAAC,CAACgD;oBACZ,IAAIA,OAAO;wBACTD,OAAOC;wBACP;oBACF;oBACA1C;gBACF;YACF;QACFJ,UAAU,CAAC,iBAAiB,EAAE2C,QAAQI,IAAI,EAAE;IAC9C;IAEA5D,UAAU;QACR,IAAIW,OAAO;YACT,MAAMA;QACR;IACF;IAEAP,GAAG,oEAAoE;QACrEQ,aAAaJ,oBAAoB,CAACqD,iBAAiB,CAAC;QAEpD,MAAMzB,WAAW,MAAM0B,MAAM,GAAGjD,QAAQ,6BAA6B,CAAC,EAAE;YACtEwB,UAAU;QACZ;QAEAlC,OAAOiC,SAASM,MAAM,EAAEqB,IAAI,CAAC;QAC7B5D,OAAOiC,SAASb,OAAO,CAACS,GAAG,CAAC,aAAa+B,IAAI,CAAC;QAC9C5D,OAAOS,aAAaJ,oBAAoB,EAAEwD,oBAAoB,CAC5D,YACA,GAAGnD,QAAQ,mBAAmB,CAAC;IAEnC;IAEAT,GAAG,wDAAwD;QACzDQ,aAAaJ,oBAAoB,CAACyD,iBAAiB,CAAC,IAAIR,MAAM;QAE9D,MAAMrB,WAAW,MAAM0B,MAAM,GAAGjD,QAAQ,6BAA6B,CAAC;QACtE,MAAMqD,OAAO,MAAM9B,SAAS+B,IAAI;QAEhChE,OAAOiC,SAASM,MAAM,EAAEqB,IAAI,CAAC;QAC7B5D,OAAO+D,MAAME,SAAS,CAAC;IACzB;IAEAhE,GAAG,2CAA2C;QAC5C,MAAMgC,WAAW,MAAM0B,MAAM,GAAGjD,QAAQ,mBAAmB,CAAC;QAC5D,MAAMqD,OAAO,MAAM9B,SAAS+B,IAAI;QAEhChE,OAAOiC,SAASM,MAAM,EAAEqB,IAAI,CAAC;QAC7B5D,OAAO+D,MAAME,SAAS,CAAC;IACzB;IAEAhE,GAAG,8CAA8C;QAC/CQ,aAAaF,cAAc,CAACmD,iBAAiB,CAAC;YAAEQ,SAAS;QAAK;QAE9D,MAAMjC,WAAW,MAAM0B,MAAM,GAAGjD,QAAQ,iDAAiD,CAAC;QAC1F,MAAMqD,OAAO,MAAM9B,SAAS+B,IAAI;QAEhChE,OAAOiC,SAASM,MAAM,EAAEqB,IAAI,CAAC;QAC7B5D,OAAO+D,MAAME,SAAS,CAAC;QACvBjE,OAAOS,aAAaF,cAAc,EAAEsD,oBAAoB,CACtD,YACA,aACA,GAAGnD,QAAQ,mBAAmB,CAAC;IAEnC;IAEAT,GAAG,6DAA6D;QAC9DQ,aAAaF,cAAc,CAACmD,iBAAiB,CAAC;YAC5CQ,SAAS;YACTV,OAAO;QACT;QAEA,MAAMvB,WAAW,MAAM0B,MAAM,GAAGjD,QAAQ,iDAAiD,CAAC;QAC1F,MAAMqD,OAAO,MAAM9B,SAAS+B,IAAI;QAEhChE,OAAOiC,SAASM,MAAM,EAAEqB,IAAI,CAAC;QAC7B5D,OAAO+D,MAAME,SAAS,CAAC;IACzB;AACF"}