@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
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration Tests: Proxy controller auth
|
|
3
3
|
*
|
|
4
|
-
* Verifies
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - McpOAuthGuard always enforces Bearer token
|
|
6
|
+
* - WWW-Authenticate header with resource_metadata_uri on 401
|
|
7
|
+
* - Valid tokens resolve user and pass through
|
|
7
8
|
* - Org-scoped profile lookup through the proxy service
|
|
8
|
-
* -
|
|
9
|
-
* - Gateway endpoint still works with handleGatewayRequest
|
|
9
|
+
* - Gateway endpoint uses default profile with user scoping
|
|
10
10
|
*/ import { NotFoundException, UnauthorizedException } from "@nestjs/common";
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
12
|
// Dynamic import to handle ESM
|
|
13
|
+
const { McpOAuthGuard } = await import("../../modules/auth/mcp-oauth.guard.js");
|
|
13
14
|
const { ProxyController } = await import("../../modules/proxy/proxy.controller.js");
|
|
14
15
|
// ────────────────────────────────────────────────
|
|
15
16
|
// Mock factories
|
|
@@ -23,6 +24,19 @@ function createMockAuthService() {
|
|
|
23
24
|
onModuleInit: vi.fn()
|
|
24
25
|
};
|
|
25
26
|
}
|
|
27
|
+
function createMockConfigService(overrides = {}) {
|
|
28
|
+
const defaults = {
|
|
29
|
+
'app.port': 3001,
|
|
30
|
+
BETTER_AUTH_URL: 'http://localhost:3001'
|
|
31
|
+
};
|
|
32
|
+
const config = {
|
|
33
|
+
...defaults,
|
|
34
|
+
...overrides
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
get: vi.fn((key)=>config[key])
|
|
38
|
+
};
|
|
39
|
+
}
|
|
26
40
|
function createMockProxyService() {
|
|
27
41
|
return {
|
|
28
42
|
handleRequest: vi.fn().mockResolvedValue({
|
|
@@ -72,120 +86,176 @@ function createMockEventEmitter() {
|
|
|
72
86
|
off: vi.fn()
|
|
73
87
|
};
|
|
74
88
|
}
|
|
75
|
-
function createMockRequest(headers = {}) {
|
|
89
|
+
function createMockRequest(headers = {}, query = {}) {
|
|
76
90
|
return {
|
|
77
91
|
headers,
|
|
78
|
-
|
|
92
|
+
query,
|
|
93
|
+
on: vi.fn(),
|
|
94
|
+
user: undefined
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
function createMockExecutionContext(req) {
|
|
98
|
+
return {
|
|
99
|
+
switchToHttp: ()=>({
|
|
100
|
+
getRequest: ()=>req,
|
|
101
|
+
getResponse: ()=>({})
|
|
102
|
+
}),
|
|
103
|
+
getHandler: ()=>({}),
|
|
104
|
+
getClass: ()=>({})
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ────────────────────────────────────────────────
|
|
108
|
+
// McpOAuthGuard tests
|
|
109
|
+
// ────────────────────────────────────────────────
|
|
110
|
+
describe('McpOAuthGuard', ()=>{
|
|
111
|
+
let guard;
|
|
83
112
|
let authService;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
beforeEach(()=>{
|
|
114
|
+
authService = createMockAuthService();
|
|
115
|
+
const configService = createMockConfigService();
|
|
116
|
+
guard = new McpOAuthGuard(authService, configService);
|
|
117
|
+
});
|
|
118
|
+
it('rejects request without Bearer token with 401', async ()=>{
|
|
119
|
+
const req = createMockRequest();
|
|
120
|
+
const ctx = createMockExecutionContext(req);
|
|
121
|
+
await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);
|
|
122
|
+
});
|
|
123
|
+
it('returns WWW-Authenticate header on missing token', async ()=>{
|
|
124
|
+
const req = createMockRequest();
|
|
125
|
+
const ctx = createMockExecutionContext(req);
|
|
126
|
+
try {
|
|
127
|
+
await guard.canActivate(ctx);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
expect(error.wwwAuthenticate).toContain('resource_metadata=');
|
|
130
|
+
expect(error.wwwAuthenticate).toContain('resource_metadata_uri=');
|
|
131
|
+
expect(error.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
it('rejects invalid Bearer token', async ()=>{
|
|
135
|
+
authService.validateMcpToken.mockResolvedValue(null);
|
|
136
|
+
const req = createMockRequest({
|
|
137
|
+
authorization: 'Bearer bad-token'
|
|
97
138
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
const ctx = createMockExecutionContext(req);
|
|
140
|
+
await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);
|
|
141
|
+
expect(authService.validateMcpToken).toHaveBeenCalledWith('bad-token');
|
|
142
|
+
});
|
|
143
|
+
it('allows valid Bearer token and attaches user', async ()=>{
|
|
144
|
+
const user = {
|
|
145
|
+
id: 'user-1',
|
|
146
|
+
name: 'Test',
|
|
147
|
+
email: 'test@example.com'
|
|
148
|
+
};
|
|
149
|
+
authService.validateMcpToken.mockResolvedValue(user);
|
|
150
|
+
const req = createMockRequest({
|
|
151
|
+
authorization: 'Bearer valid-token'
|
|
107
152
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
const ctx = createMockExecutionContext(req);
|
|
154
|
+
const result = await guard.canActivate(ctx);
|
|
155
|
+
expect(result).toBe(true);
|
|
156
|
+
expect(req.user).toEqual(user);
|
|
157
|
+
});
|
|
158
|
+
it('accepts access_token query param (SSE fallback)', async ()=>{
|
|
159
|
+
const user = {
|
|
160
|
+
id: 'user-1',
|
|
161
|
+
name: 'Test',
|
|
162
|
+
email: 'test@example.com'
|
|
163
|
+
};
|
|
164
|
+
authService.validateMcpToken.mockResolvedValue(user);
|
|
165
|
+
const req = createMockRequest({}, {
|
|
166
|
+
access_token: 'sse-token'
|
|
117
167
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
168
|
+
const ctx = createMockExecutionContext(req);
|
|
169
|
+
const result = await guard.canActivate(ctx);
|
|
170
|
+
expect(result).toBe(true);
|
|
171
|
+
expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');
|
|
172
|
+
});
|
|
173
|
+
it('accepts session cookie when no Bearer token is present', async ()=>{
|
|
174
|
+
const user = {
|
|
175
|
+
id: 'session-user',
|
|
176
|
+
name: 'Session',
|
|
177
|
+
email: 'session@example.com'
|
|
178
|
+
};
|
|
179
|
+
authService.getSession.mockResolvedValue({
|
|
180
|
+
user,
|
|
181
|
+
session: {
|
|
182
|
+
id: 's1',
|
|
183
|
+
userId: user.id
|
|
184
|
+
}
|
|
130
185
|
});
|
|
186
|
+
const req = createMockRequest({
|
|
187
|
+
cookie: 'better-auth.session_token=abc123'
|
|
188
|
+
});
|
|
189
|
+
const ctx = createMockExecutionContext(req);
|
|
190
|
+
const result = await guard.canActivate(ctx);
|
|
191
|
+
expect(result).toBe(true);
|
|
192
|
+
expect(req.user).toEqual(user);
|
|
193
|
+
expect(authService.validateMcpToken).not.toHaveBeenCalled();
|
|
131
194
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
195
|
+
it('rejects when neither Bearer token nor session cookie is valid', async ()=>{
|
|
196
|
+
authService.getSession.mockResolvedValue(null);
|
|
197
|
+
const req = createMockRequest({
|
|
198
|
+
cookie: 'better-auth.session_token=expired'
|
|
199
|
+
});
|
|
200
|
+
const ctx = createMockExecutionContext(req);
|
|
201
|
+
await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);
|
|
202
|
+
});
|
|
203
|
+
it('does not fall through to session cookie when Bearer token is invalid', async ()=>{
|
|
204
|
+
const user = {
|
|
205
|
+
id: 'session-user',
|
|
206
|
+
name: 'Session',
|
|
207
|
+
email: 'session@example.com'
|
|
140
208
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
209
|
+
authService.validateMcpToken.mockResolvedValue(null);
|
|
210
|
+
authService.getSession.mockResolvedValue({
|
|
211
|
+
user,
|
|
212
|
+
session: {
|
|
213
|
+
id: 's1',
|
|
214
|
+
userId: user.id
|
|
215
|
+
}
|
|
147
216
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const req = createMockRequest({
|
|
151
|
-
authorization: 'Bearer valid-mcp-token'
|
|
152
|
-
});
|
|
153
|
-
const mcpRequest = {
|
|
154
|
-
jsonrpc: '2.0',
|
|
155
|
-
id: 1,
|
|
156
|
-
method: 'tools/list'
|
|
157
|
-
};
|
|
158
|
-
await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);
|
|
159
|
-
expect(authService.validateMcpToken).toHaveBeenCalledWith('valid-mcp-token');
|
|
160
|
-
expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', mcpRequest, 'user-1');
|
|
217
|
+
const req = createMockRequest({
|
|
218
|
+
authorization: 'Bearer bad-token'
|
|
161
219
|
});
|
|
162
|
-
|
|
163
|
-
|
|
220
|
+
const ctx = createMockExecutionContext(req);
|
|
221
|
+
await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);
|
|
222
|
+
expect(authService.getSession).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
// ────────────────────────────────────────────────
|
|
226
|
+
// Proxy controller auth tests
|
|
227
|
+
// ────────────────────────────────────────────────
|
|
228
|
+
describe('Proxy controller auth', ()=>{
|
|
229
|
+
let controller;
|
|
230
|
+
let proxyService;
|
|
231
|
+
let settingsService;
|
|
232
|
+
let eventEmitter;
|
|
233
|
+
beforeEach(()=>{
|
|
234
|
+
proxyService = createMockProxyService();
|
|
235
|
+
settingsService = createMockSettingsService();
|
|
236
|
+
eventEmitter = createMockEventEmitter();
|
|
237
|
+
controller = new ProxyController(proxyService, settingsService, eventEmitter);
|
|
238
|
+
});
|
|
239
|
+
describe('guard-authenticated user passthrough', ()=>{
|
|
240
|
+
it('uses req.user set by McpOAuthGuard', async ()=>{
|
|
241
|
+
const guardUser = {
|
|
242
|
+
id: 'guard-user',
|
|
243
|
+
name: 'Guard',
|
|
244
|
+
email: 'g@test.com'
|
|
245
|
+
};
|
|
164
246
|
const req = createMockRequest({
|
|
165
|
-
authorization: 'Bearer
|
|
247
|
+
authorization: 'Bearer some-token'
|
|
166
248
|
});
|
|
167
|
-
|
|
168
|
-
jsonrpc: '2.0',
|
|
169
|
-
id: 1,
|
|
170
|
-
method: 'tools/list'
|
|
171
|
-
};
|
|
172
|
-
await expect(controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest)).rejects.toThrow(UnauthorizedException);
|
|
173
|
-
});
|
|
174
|
-
it('no Authorization header falls back to __unauthenticated__', async ()=>{
|
|
175
|
-
const req = createMockRequest();
|
|
249
|
+
req.user = guardUser;
|
|
176
250
|
const mcpRequest = {
|
|
177
251
|
jsonrpc: '2.0',
|
|
178
252
|
id: 1,
|
|
179
253
|
method: 'tools/list'
|
|
180
254
|
};
|
|
181
255
|
await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);
|
|
182
|
-
expect(
|
|
183
|
-
expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', mcpRequest, '__unauthenticated__');
|
|
256
|
+
expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', mcpRequest, 'guard-user');
|
|
184
257
|
});
|
|
185
258
|
});
|
|
186
|
-
// ────────────────────────────────────────────────
|
|
187
|
-
// Org-scoped profile lookup through proxy
|
|
188
|
-
// ────────────────────────────────────────────────
|
|
189
259
|
describe('org-scoped profile lookup', ()=>{
|
|
190
260
|
const userA = {
|
|
191
261
|
id: 'user-a',
|
|
@@ -197,18 +267,11 @@ describe('Proxy controller auth', ()=>{
|
|
|
197
267
|
name: 'User B',
|
|
198
268
|
email: 'b@test.com'
|
|
199
269
|
};
|
|
200
|
-
beforeEach(()=>{
|
|
201
|
-
authService = createMockAuthService();
|
|
202
|
-
proxyService = createMockProxyService();
|
|
203
|
-
settingsService = createMockSettingsService();
|
|
204
|
-
eventEmitter = createMockEventEmitter();
|
|
205
|
-
controller = new ProxyController(proxyService, settingsService, eventEmitter, authService);
|
|
206
|
-
});
|
|
207
270
|
it('org slug and user are passed through to proxy service', async ()=>{
|
|
208
|
-
authService.validateMcpToken.mockResolvedValue(userA);
|
|
209
271
|
const req = createMockRequest({
|
|
210
272
|
authorization: 'Bearer token-a'
|
|
211
273
|
});
|
|
274
|
+
req.user = userA;
|
|
212
275
|
const mcpRequest = {
|
|
213
276
|
jsonrpc: '2.0',
|
|
214
277
|
id: 1,
|
|
@@ -218,32 +281,30 @@ describe('Proxy controller auth', ()=>{
|
|
|
218
281
|
expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('user-a-profile', 'org-a', mcpRequest, 'user-a');
|
|
219
282
|
});
|
|
220
283
|
it('different users get different userId passed to proxy', async ()=>{
|
|
221
|
-
// First request from User A
|
|
222
|
-
authService.validateMcpToken.mockResolvedValue(userA);
|
|
223
284
|
const reqA = createMockRequest({
|
|
224
285
|
authorization: 'Bearer token-a'
|
|
225
286
|
});
|
|
287
|
+
reqA.user = userA;
|
|
226
288
|
const mcpReq = {
|
|
227
289
|
jsonrpc: '2.0',
|
|
228
290
|
id: 1,
|
|
229
291
|
method: 'tools/list'
|
|
230
292
|
};
|
|
231
293
|
await controller.handleOrgMcpRequest(reqA, 'shared-org', 'shared-profile', mcpReq);
|
|
232
|
-
// Second request from User B
|
|
233
|
-
authService.validateMcpToken.mockResolvedValue(userB);
|
|
234
294
|
const reqB = createMockRequest({
|
|
235
295
|
authorization: 'Bearer token-b'
|
|
236
296
|
});
|
|
297
|
+
reqB.user = userB;
|
|
237
298
|
await controller.handleOrgMcpRequest(reqB, 'shared-org', 'shared-profile', mcpReq);
|
|
238
299
|
expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(1, 'shared-profile', 'shared-org', mcpReq, 'user-a');
|
|
239
300
|
expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(2, 'shared-profile', 'shared-org', mcpReq, 'user-b');
|
|
240
301
|
});
|
|
241
302
|
it('gateway endpoint uses default profile with user scoping', async ()=>{
|
|
242
|
-
authService.validateMcpToken.mockResolvedValue(userA);
|
|
243
303
|
settingsService.getDefaultGatewayProfile.mockResolvedValue('my-default');
|
|
244
304
|
const req = createMockRequest({
|
|
245
305
|
authorization: 'Bearer token-a'
|
|
246
306
|
});
|
|
307
|
+
req.user = userA;
|
|
247
308
|
const mcpRequest = {
|
|
248
309
|
jsonrpc: '2.0',
|
|
249
310
|
id: 1,
|
|
@@ -253,12 +314,12 @@ describe('Proxy controller auth', ()=>{
|
|
|
253
314
|
expect(proxyService.handleRequest).toHaveBeenCalledWith('my-default', mcpRequest, 'user-a');
|
|
254
315
|
});
|
|
255
316
|
it('gateway wraps NotFoundException with descriptive message', async ()=>{
|
|
256
|
-
authService.validateMcpToken.mockResolvedValue(userA);
|
|
257
317
|
settingsService.getDefaultGatewayProfile.mockResolvedValue('missing-profile');
|
|
258
318
|
proxyService.handleRequest.mockRejectedValue(new NotFoundException('Profile "missing-profile" not found'));
|
|
259
319
|
const req = createMockRequest({
|
|
260
320
|
authorization: 'Bearer token-a'
|
|
261
321
|
});
|
|
322
|
+
req.user = userA;
|
|
262
323
|
const mcpRequest = {
|
|
263
324
|
jsonrpc: '2.0',
|
|
264
325
|
id: 1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/__tests__/integration/proxy-auth.test.ts"],"sourcesContent":["/**\n * Integration Tests: Proxy controller auth\n *\n * Verifies that:\n * - Proxy routes are @Public() (no session cookie required)\n * - Bearer token validation via resolveUser flow\n * - Org-scoped profile lookup through the proxy service\n * - Unauthenticated access uses __unauthenticated__ sentinel\n * - Gateway endpoint still works with handleGatewayRequest\n */\n\nimport { NotFoundException, UnauthorizedException } from '@nestjs/common';\nimport type { EventEmitter2 } from '@nestjs/event-emitter';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { type AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\nimport type { McpRequest, McpResponse, ProxyService } from '../../modules/proxy/proxy.service.js';\nimport type { SettingsService } from '../../modules/settings/settings.service.js';\n\n// Dynamic import to handle ESM\nconst { ProxyController } = await import('../../modules/proxy/proxy.controller.js');\n\n// ────────────────────────────────────────────────\n// Mock factories\n// ────────────────────────────────────────────────\n\nfunction createMockAuthService() {\n return {\n getAuth: vi.fn(),\n getSession: vi.fn().mockResolvedValue(null),\n validateMcpToken: vi.fn().mockResolvedValue(null),\n getUserOrganizations: vi.fn().mockResolvedValue([]),\n onModuleInit: vi.fn(),\n };\n}\n\nfunction createMockProxyService() {\n return {\n handleRequest: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n handleRequestByOrgSlug: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n getProfileInfo: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getProfileInfoByOrgSlug: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getToolsForServer: vi.fn().mockResolvedValue([]),\n };\n}\n\nfunction createMockSettingsService() {\n return {\n getDefaultGatewayProfile: vi.fn().mockResolvedValue('default'),\n getSetting: vi.fn(),\n setSetting: vi.fn(),\n };\n}\n\nfunction createMockEventEmitter() {\n return {\n emit: vi.fn(),\n on: vi.fn(),\n off: vi.fn(),\n };\n}\n\nfunction createMockRequest(headers: Record<string, string> = {}) {\n return {\n headers,\n on: vi.fn(),\n } as unknown as import('express').Request;\n}\n\ndescribe('Proxy controller auth', () => {\n let controller: InstanceType<typeof ProxyController>;\n let authService: ReturnType<typeof createMockAuthService>;\n let proxyService: ReturnType<typeof createMockProxyService>;\n let settingsService: ReturnType<typeof createMockSettingsService>;\n let eventEmitter: ReturnType<typeof createMockEventEmitter>;\n\n // ────────────────────────────────────────────────\n // Unauthenticated access (no Bearer token)\n // ────────────────────────────────────────────────\n\n describe('unauthenticated access (no Bearer token)', () => {\n beforeEach(() => {\n authService = createMockAuthService();\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2,\n authService as unknown as AuthService\n );\n });\n\n it('org-scoped POST request passes __unauthenticated__ to proxy service', async () => {\n const req = createMockRequest();\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n '__unauthenticated__'\n );\n });\n\n it('gateway POST request passes __unauthenticated__', async () => {\n const req = createMockRequest();\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n };\n\n await controller.handleGatewayRequest(req, mcpRequest);\n\n expect(proxyService.handleRequest).toHaveBeenCalledWith(\n 'default',\n mcpRequest,\n '__unauthenticated__'\n );\n });\n\n it('non-Bearer Authorization header falls back to __unauthenticated__', async () => {\n const req = createMockRequest({\n authorization: 'Basic dXNlcjpwYXNz',\n });\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(authService.validateMcpToken).not.toHaveBeenCalled();\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n '__unauthenticated__'\n );\n });\n });\n\n // ────────────────────────────────────────────────\n // Authenticated access with Bearer tokens\n // ────────────────────────────────────────────────\n\n describe('authenticated access with Bearer tokens', () => {\n const validUser: AuthUser = {\n id: 'user-1',\n name: 'Token User',\n email: 'token@example.com',\n };\n\n beforeEach(() => {\n authService = createMockAuthService();\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2,\n authService as unknown as AuthService\n );\n });\n\n it('valid Bearer token resolves to authenticated user for org-scoped request', async () => {\n authService.validateMcpToken.mockResolvedValue(validUser);\n const req = createMockRequest({\n authorization: 'Bearer valid-mcp-token',\n });\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(authService.validateMcpToken).toHaveBeenCalledWith('valid-mcp-token');\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n 'user-1'\n );\n });\n\n it('invalid Bearer token throws UnauthorizedException', async () => {\n authService.validateMcpToken.mockResolvedValue(null);\n const req = createMockRequest({\n authorization: 'Bearer invalid-token',\n });\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n };\n\n await expect(\n controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest)\n ).rejects.toThrow(UnauthorizedException);\n });\n\n it('no Authorization header falls back to __unauthenticated__', async () => {\n const req = createMockRequest();\n const mcpRequest: McpRequest = {\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(authService.validateMcpToken).not.toHaveBeenCalled();\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n '__unauthenticated__'\n );\n });\n });\n\n // ────────────────────────────────────────────────\n // Org-scoped profile lookup through proxy\n // ────────────────────────────────────────────────\n\n describe('org-scoped profile lookup', () => {\n const userA: AuthUser = { id: 'user-a', name: 'User A', email: 'a@test.com' };\n const userB: AuthUser = { id: 'user-b', name: 'User B', email: 'b@test.com' };\n\n beforeEach(() => {\n authService = createMockAuthService();\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2,\n authService as unknown as AuthService\n );\n });\n\n it('org slug and user are passed through to proxy service', async () => {\n authService.validateMcpToken.mockResolvedValue(userA);\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'org-a', 'user-a-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'user-a-profile',\n 'org-a',\n mcpRequest,\n 'user-a'\n );\n });\n\n it('different users get different userId passed to proxy', async () => {\n // First request from User A\n authService.validateMcpToken.mockResolvedValue(userA);\n const reqA = createMockRequest({ authorization: 'Bearer token-a' });\n const mcpReq: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(reqA, 'shared-org', 'shared-profile', mcpReq);\n\n // Second request from User B\n authService.validateMcpToken.mockResolvedValue(userB);\n const reqB = createMockRequest({ authorization: 'Bearer token-b' });\n\n await controller.handleOrgMcpRequest(reqB, 'shared-org', 'shared-profile', mcpReq);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 1,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-a'\n );\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 2,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-b'\n );\n });\n\n it('gateway endpoint uses default profile with user scoping', async () => {\n authService.validateMcpToken.mockResolvedValue(userA);\n settingsService.getDefaultGatewayProfile.mockResolvedValue('my-default');\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleGatewayRequest(req, mcpRequest);\n\n expect(proxyService.handleRequest).toHaveBeenCalledWith('my-default', mcpRequest, 'user-a');\n });\n\n it('gateway wraps NotFoundException with descriptive message', async () => {\n authService.validateMcpToken.mockResolvedValue(userA);\n settingsService.getDefaultGatewayProfile.mockResolvedValue('missing-profile');\n proxyService.handleRequest.mockRejectedValue(\n new NotFoundException('Profile \"missing-profile\" not found')\n );\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await expect(controller.handleGatewayRequest(req, mcpRequest)).rejects.toThrow(\n NotFoundException\n );\n });\n\n it('org-scoped profile info endpoint uses orgSlug', async () => {\n await controller.getOrgProfileInfo('my-org', 'my-profile');\n\n expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org');\n });\n });\n});\n"],"names":["NotFoundException","UnauthorizedException","beforeEach","describe","expect","it","vi","ProxyController","createMockAuthService","getAuth","fn","getSession","mockResolvedValue","validateMcpToken","getUserOrganizations","onModuleInit","createMockProxyService","handleRequest","jsonrpc","id","result","tools","handleRequestByOrgSlug","getProfileInfo","serverStatus","total","connected","servers","getProfileInfoByOrgSlug","getToolsForServer","createMockSettingsService","getDefaultGatewayProfile","getSetting","setSetting","createMockEventEmitter","emit","on","off","createMockRequest","headers","controller","authService","proxyService","settingsService","eventEmitter","req","mcpRequest","method","handleOrgMcpRequest","toHaveBeenCalledWith","handleGatewayRequest","authorization","not","toHaveBeenCalled","validUser","name","email","rejects","toThrow","userA","userB","reqA","mcpReq","reqB","toHaveBeenNthCalledWith","mockRejectedValue","getOrgProfileInfo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/integration/proxy-auth.test.ts"],"sourcesContent":["/**\n * Integration Tests: Proxy controller auth\n *\n * Verifies:\n * - McpOAuthGuard always enforces Bearer token\n * - WWW-Authenticate header with resource_metadata_uri on 401\n * - Valid tokens resolve user and pass through\n * - Org-scoped profile lookup through the proxy service\n * - Gateway endpoint uses default profile with user scoping\n */\n\nimport { NotFoundException, UnauthorizedException } from '@nestjs/common';\nimport type { EventEmitter2 } from '@nestjs/event-emitter';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { type AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\nimport type { McpRequest, McpResponse, ProxyService } from '../../modules/proxy/proxy.service.js';\nimport type { SettingsService } from '../../modules/settings/settings.service.js';\n\n// Dynamic import to handle ESM\nconst { McpOAuthGuard } = await import('../../modules/auth/mcp-oauth.guard.js');\nconst { ProxyController } = await import('../../modules/proxy/proxy.controller.js');\n\n// ────────────────────────────────────────────────\n// Mock factories\n// ────────────────────────────────────────────────\n\nfunction createMockAuthService() {\n return {\n getAuth: vi.fn(),\n getSession: vi.fn().mockResolvedValue(null),\n validateMcpToken: vi.fn().mockResolvedValue(null),\n getUserOrganizations: vi.fn().mockResolvedValue([]),\n onModuleInit: vi.fn(),\n };\n}\n\nfunction createMockConfigService(overrides: Record<string, unknown> = {}) {\n const defaults: Record<string, unknown> = {\n 'app.port': 3001,\n BETTER_AUTH_URL: 'http://localhost:3001',\n };\n const config = { ...defaults, ...overrides };\n return {\n get: vi.fn((key: string) => config[key]),\n };\n}\n\nfunction createMockProxyService() {\n return {\n handleRequest: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n handleRequestByOrgSlug: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n getProfileInfo: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getProfileInfoByOrgSlug: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getToolsForServer: vi.fn().mockResolvedValue([]),\n };\n}\n\nfunction createMockSettingsService() {\n return {\n getDefaultGatewayProfile: vi.fn().mockResolvedValue('default'),\n getSetting: vi.fn(),\n setSetting: vi.fn(),\n };\n}\n\nfunction createMockEventEmitter() {\n return {\n emit: vi.fn(),\n on: vi.fn(),\n off: vi.fn(),\n };\n}\n\nfunction createMockRequest(headers: Record<string, string> = {}, query: Record<string, string> = {}) {\n return {\n headers,\n query,\n on: vi.fn(),\n user: undefined as any,\n } as unknown as import('express').Request;\n}\n\nfunction createMockExecutionContext(req: import('express').Request) {\n return {\n switchToHttp: () => ({\n getRequest: () => req,\n getResponse: () => ({}),\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as any;\n}\n\n// ────────────────────────────────────────────────\n// McpOAuthGuard tests\n// ────────────────────────────────────────────────\n\ndescribe('McpOAuthGuard', () => {\n let guard: InstanceType<typeof McpOAuthGuard>;\n let authService: ReturnType<typeof createMockAuthService>;\n\n beforeEach(() => {\n authService = createMockAuthService();\n const configService = createMockConfigService();\n guard = new McpOAuthGuard(\n authService as unknown as AuthService,\n configService as any\n );\n });\n\n it('rejects request without Bearer token with 401', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('returns WWW-Authenticate header on missing token', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n try {\n await guard.canActivate(ctx);\n } catch (error: any) {\n expect(error.wwwAuthenticate).toContain('resource_metadata=');\n expect(error.wwwAuthenticate).toContain('resource_metadata_uri=');\n expect(error.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');\n }\n });\n\n it('rejects invalid Bearer token', async () => {\n authService.validateMcpToken.mockResolvedValue(null);\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('bad-token');\n });\n\n it('allows valid Bearer token and attaches user', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({ authorization: 'Bearer valid-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n });\n\n it('accepts access_token query param (SSE fallback)', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({}, { access_token: 'sse-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');\n });\n\n it('accepts session cookie when no Bearer token is present', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ cookie: 'better-auth.session_token=abc123' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n expect(authService.validateMcpToken).not.toHaveBeenCalled();\n });\n\n it('rejects when neither Bearer token nor session cookie is valid', async () => {\n authService.getSession.mockResolvedValue(null);\n const req = createMockRequest({ cookie: 'better-auth.session_token=expired' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('does not fall through to session cookie when Bearer token is invalid', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.validateMcpToken.mockResolvedValue(null);\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n});\n\n// ────────────────────────────────────────────────\n// Proxy controller auth tests\n// ────────────────────────────────────────────────\n\ndescribe('Proxy controller auth', () => {\n let controller: InstanceType<typeof ProxyController>;\n let proxyService: ReturnType<typeof createMockProxyService>;\n let settingsService: ReturnType<typeof createMockSettingsService>;\n let eventEmitter: ReturnType<typeof createMockEventEmitter>;\n\n beforeEach(() => {\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2\n );\n });\n\n describe('guard-authenticated user passthrough', () => {\n it('uses req.user set by McpOAuthGuard', async () => {\n const guardUser: AuthUser = { id: 'guard-user', name: 'Guard', email: 'g@test.com' };\n const req = createMockRequest({ authorization: 'Bearer some-token' });\n (req as any).user = guardUser;\n\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n 'guard-user'\n );\n });\n });\n\n describe('org-scoped profile lookup', () => {\n const userA: AuthUser = { id: 'user-a', name: 'User A', email: 'a@test.com' };\n const userB: AuthUser = { id: 'user-b', name: 'User B', email: 'b@test.com' };\n\n it('org slug and user are passed through to proxy service', async () => {\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'org-a', 'user-a-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'user-a-profile',\n 'org-a',\n mcpRequest,\n 'user-a'\n );\n });\n\n it('different users get different userId passed to proxy', async () => {\n const reqA = createMockRequest({ authorization: 'Bearer token-a' });\n (reqA as any).user = userA;\n const mcpReq: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(reqA, 'shared-org', 'shared-profile', mcpReq);\n\n const reqB = createMockRequest({ authorization: 'Bearer token-b' });\n (reqB as any).user = userB;\n\n await controller.handleOrgMcpRequest(reqB, 'shared-org', 'shared-profile', mcpReq);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 1,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-a'\n );\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 2,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-b'\n );\n });\n\n it('gateway endpoint uses default profile with user scoping', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('my-default');\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleGatewayRequest(req, mcpRequest);\n\n expect(proxyService.handleRequest).toHaveBeenCalledWith('my-default', mcpRequest, 'user-a');\n });\n\n it('gateway wraps NotFoundException with descriptive message', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('missing-profile');\n proxyService.handleRequest.mockRejectedValue(\n new NotFoundException('Profile \"missing-profile\" not found')\n );\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await expect(controller.handleGatewayRequest(req, mcpRequest)).rejects.toThrow(\n NotFoundException\n );\n });\n\n it('org-scoped profile info endpoint uses orgSlug', async () => {\n await controller.getOrgProfileInfo('my-org', 'my-profile');\n\n expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org');\n });\n });\n});\n"],"names":["NotFoundException","UnauthorizedException","beforeEach","describe","expect","it","vi","McpOAuthGuard","ProxyController","createMockAuthService","getAuth","fn","getSession","mockResolvedValue","validateMcpToken","getUserOrganizations","onModuleInit","createMockConfigService","overrides","defaults","BETTER_AUTH_URL","config","get","key","createMockProxyService","handleRequest","jsonrpc","id","result","tools","handleRequestByOrgSlug","getProfileInfo","serverStatus","total","connected","servers","getProfileInfoByOrgSlug","getToolsForServer","createMockSettingsService","getDefaultGatewayProfile","getSetting","setSetting","createMockEventEmitter","emit","on","off","createMockRequest","headers","query","user","undefined","createMockExecutionContext","req","switchToHttp","getRequest","getResponse","getHandler","getClass","guard","authService","configService","ctx","canActivate","rejects","toThrow","error","wwwAuthenticate","toContain","authorization","toHaveBeenCalledWith","name","email","toBe","toEqual","access_token","session","userId","cookie","not","toHaveBeenCalled","controller","proxyService","settingsService","eventEmitter","guardUser","mcpRequest","method","handleOrgMcpRequest","userA","userB","reqA","mcpReq","reqB","toHaveBeenNthCalledWith","handleGatewayRequest","mockRejectedValue","getOrgProfileInfo"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for AuthGuard
|
|
3
|
-
*/ import { UnauthorizedException } from "@nestjs/common";
|
|
3
|
+
*/ import { ForbiddenException, UnauthorizedException } from "@nestjs/common";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { AuthGuard } from "../../modules/auth/auth.guard.js";
|
|
6
6
|
function createMockExecutionContext(headers = {}) {
|
|
@@ -54,7 +54,7 @@ describe('AuthGuard', ()=>{
|
|
|
54
54
|
const mockSession = {
|
|
55
55
|
id: 'sess-1',
|
|
56
56
|
userId: 'user-1',
|
|
57
|
-
activeOrganizationId:
|
|
57
|
+
activeOrganizationId: 'org-1'
|
|
58
58
|
};
|
|
59
59
|
authService.getSession.mockResolvedValue({
|
|
60
60
|
user: mockUser,
|
|
@@ -69,6 +69,27 @@ describe('AuthGuard', ()=>{
|
|
|
69
69
|
expect(request.user).toEqual(mockUser);
|
|
70
70
|
expect(request.sessionData).toEqual(mockSession);
|
|
71
71
|
});
|
|
72
|
+
it('should throw ForbiddenException when session has no active organization', async ()=>{
|
|
73
|
+
const mockUser = {
|
|
74
|
+
id: 'user-1',
|
|
75
|
+
name: 'Test',
|
|
76
|
+
email: 'test@example.com',
|
|
77
|
+
image: null
|
|
78
|
+
};
|
|
79
|
+
const mockSession = {
|
|
80
|
+
id: 'sess-1',
|
|
81
|
+
userId: 'user-1',
|
|
82
|
+
activeOrganizationId: null
|
|
83
|
+
};
|
|
84
|
+
authService.getSession.mockResolvedValue({
|
|
85
|
+
user: mockUser,
|
|
86
|
+
session: mockSession
|
|
87
|
+
});
|
|
88
|
+
const ctx = createMockExecutionContext({
|
|
89
|
+
cookie: 'session=abc'
|
|
90
|
+
});
|
|
91
|
+
await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);
|
|
92
|
+
});
|
|
72
93
|
});
|
|
73
94
|
|
|
74
95
|
//# sourceMappingURL=auth.guard.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/__tests__/unit/auth.guard.test.ts"],"sourcesContent":["/**\n * Tests for AuthGuard\n */\n\nimport { ExecutionContext, UnauthorizedException } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AuthGuard } from '../../modules/auth/auth.guard.js';\nimport { AuthService } from '../../modules/auth/auth.service.js';\n\nfunction createMockExecutionContext(headers: Record<string, string> = {}): ExecutionContext {\n const request = { headers, user: undefined, sessionData: undefined };\n return {\n switchToHttp: () => ({\n getRequest: () => request,\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as unknown as ExecutionContext;\n}\n\ndescribe('AuthGuard', () => {\n let guard: AuthGuard;\n let authService: { getSession: ReturnType<typeof vi.fn> };\n let reflector: { getAllAndOverride: ReturnType<typeof vi.fn> };\n\n beforeEach(() => {\n authService = {\n getSession: vi.fn(),\n };\n reflector = {\n getAllAndOverride: vi.fn().mockReturnValue(false),\n };\n guard = new AuthGuard(authService as unknown as AuthService, reflector as unknown as Reflector);\n });\n\n it('should allow @Public() routes without auth', async () => {\n reflector.getAllAndOverride.mockReturnValue(true);\n const ctx = createMockExecutionContext();\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n\n it('should throw UnauthorizedException when no session', async () => {\n authService.getSession.mockResolvedValue(null);\n const ctx = createMockExecutionContext({ cookie: 'session=abc' });\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('should attach user and session when valid session exists', async () => {\n const mockUser = { id: 'user-1', name: 'Test', email: 'test@example.com', image: null };\n const mockSession = { id: 'sess-1', userId: 'user-1', activeOrganizationId:
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/unit/auth.guard.test.ts"],"sourcesContent":["/**\n * Tests for AuthGuard\n */\n\nimport { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AuthGuard } from '../../modules/auth/auth.guard.js';\nimport { AuthService } from '../../modules/auth/auth.service.js';\n\nfunction createMockExecutionContext(headers: Record<string, string> = {}): ExecutionContext {\n const request = { headers, user: undefined, sessionData: undefined };\n return {\n switchToHttp: () => ({\n getRequest: () => request,\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as unknown as ExecutionContext;\n}\n\ndescribe('AuthGuard', () => {\n let guard: AuthGuard;\n let authService: { getSession: ReturnType<typeof vi.fn> };\n let reflector: { getAllAndOverride: ReturnType<typeof vi.fn> };\n\n beforeEach(() => {\n authService = {\n getSession: vi.fn(),\n };\n reflector = {\n getAllAndOverride: vi.fn().mockReturnValue(false),\n };\n guard = new AuthGuard(authService as unknown as AuthService, reflector as unknown as Reflector);\n });\n\n it('should allow @Public() routes without auth', async () => {\n reflector.getAllAndOverride.mockReturnValue(true);\n const ctx = createMockExecutionContext();\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n\n it('should throw UnauthorizedException when no session', async () => {\n authService.getSession.mockResolvedValue(null);\n const ctx = createMockExecutionContext({ cookie: 'session=abc' });\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('should attach user and session when valid session exists', async () => {\n const mockUser = { id: 'user-1', name: 'Test', email: 'test@example.com', image: null };\n const mockSession = { id: 'sess-1', userId: 'user-1', activeOrganizationId: 'org-1' };\n authService.getSession.mockResolvedValue({ user: mockUser, session: mockSession });\n\n const ctx = createMockExecutionContext({ cookie: 'session=abc' });\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n const request = ctx.switchToHttp().getRequest();\n expect(request.user).toEqual(mockUser);\n expect(request.sessionData).toEqual(mockSession);\n });\n\n it('should throw ForbiddenException when session has no active organization', async () => {\n const mockUser = { id: 'user-1', name: 'Test', email: 'test@example.com', image: null };\n const mockSession = { id: 'sess-1', userId: 'user-1', activeOrganizationId: null };\n authService.getSession.mockResolvedValue({ user: mockUser, session: mockSession });\n\n const ctx = createMockExecutionContext({ cookie: 'session=abc' });\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);\n });\n});\n"],"names":["ForbiddenException","UnauthorizedException","beforeEach","describe","expect","it","vi","AuthGuard","createMockExecutionContext","headers","request","user","undefined","sessionData","switchToHttp","getRequest","getHandler","getClass","guard","authService","reflector","getSession","fn","getAllAndOverride","mockReturnValue","ctx","result","canActivate","toBe","not","toHaveBeenCalled","mockResolvedValue","cookie","rejects","toThrow","mockUser","id","name","email","image","mockSession","userId","activeOrganizationId","session","toEqual"],"mappings":"AAAA;;CAEC,GAED,SAA2BA,kBAAkB,EAAEC,qBAAqB,QAAQ,iBAAiB;AAE7F,SAASC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAC9D,SAASC,SAAS,QAAQ,mCAAmC;AAG7D,SAASC,2BAA2BC,UAAkC,CAAC,CAAC;IACtE,MAAMC,UAAU;QAAED;QAASE,MAAMC;QAAWC,aAAaD;IAAU;IACnE,OAAO;QACLE,cAAc,IAAO,CAAA;gBACnBC,YAAY,IAAML;YACpB,CAAA;QACAM,YAAY,IAAO,CAAA,CAAC,CAAA;QACpBC,UAAU,IAAO,CAAA,CAAC,CAAA;IACpB;AACF;AAEAd,SAAS,aAAa;IACpB,IAAIe;IACJ,IAAIC;IACJ,IAAIC;IAEJlB,WAAW;QACTiB,cAAc;YACZE,YAAYf,GAAGgB,EAAE;QACnB;QACAF,YAAY;YACVG,mBAAmBjB,GAAGgB,EAAE,GAAGE,eAAe,CAAC;QAC7C;QACAN,QAAQ,IAAIX,UAAUY,aAAuCC;IAC/D;IAEAf,GAAG,8CAA8C;QAC/Ce,UAAUG,iBAAiB,CAACC,eAAe,CAAC;QAC5C,MAAMC,MAAMjB;QACZ,MAAMkB,SAAS,MAAMR,MAAMS,WAAW,CAACF;QAEvCrB,OAAOsB,QAAQE,IAAI,CAAC;QACpBxB,OAAOe,YAAYE,UAAU,EAAEQ,GAAG,CAACC,gBAAgB;IACrD;IAEAzB,GAAG,sDAAsD;QACvDc,YAAYE,UAAU,CAACU,iBAAiB,CAAC;QACzC,MAAMN,MAAMjB,2BAA2B;YAAEwB,QAAQ;QAAc;QAE/D,MAAM5B,OAAOc,MAAMS,WAAW,CAACF,MAAMQ,OAAO,CAACC,OAAO,CAACjC;IACvD;IAEAI,GAAG,4DAA4D;QAC7D,MAAM8B,WAAW;YAAEC,IAAI;YAAUC,MAAM;YAAQC,OAAO;YAAoBC,OAAO;QAAK;QACtF,MAAMC,cAAc;YAAEJ,IAAI;YAAUK,QAAQ;YAAUC,sBAAsB;QAAQ;QACpFvB,YAAYE,UAAU,CAACU,iBAAiB,CAAC;YAAEpB,MAAMwB;YAAUQ,SAASH;QAAY;QAEhF,MAAMf,MAAMjB,2BAA2B;YAAEwB,QAAQ;QAAc;QAC/D,MAAMN,SAAS,MAAMR,MAAMS,WAAW,CAACF;QAEvCrB,OAAOsB,QAAQE,IAAI,CAAC;QACpB,MAAMlB,UAAUe,IAAIX,YAAY,GAAGC,UAAU;QAC7CX,OAAOM,QAAQC,IAAI,EAAEiC,OAAO,CAACT;QAC7B/B,OAAOM,QAAQG,WAAW,EAAE+B,OAAO,CAACJ;IACtC;IAEAnC,GAAG,2EAA2E;QAC5E,MAAM8B,WAAW;YAAEC,IAAI;YAAUC,MAAM;YAAQC,OAAO;YAAoBC,OAAO;QAAK;QACtF,MAAMC,cAAc;YAAEJ,IAAI;YAAUK,QAAQ;YAAUC,sBAAsB;QAAK;QACjFvB,YAAYE,UAAU,CAACU,iBAAiB,CAAC;YAAEpB,MAAMwB;YAAUQ,SAASH;QAAY;QAEhF,MAAMf,MAAMjB,2BAA2B;YAAEwB,QAAQ;QAAc;QAE/D,MAAM5B,OAAOc,MAAMS,WAAW,CAACF,MAAMQ,OAAO,CAACC,OAAO,CAAClC;IACvD;AACF"}
|
|
@@ -48,6 +48,12 @@ export class AllExceptionsFilter {
|
|
|
48
48
|
if (typeof requestId === 'string') {
|
|
49
49
|
errorResponse.requestId = requestId;
|
|
50
50
|
}
|
|
51
|
+
// Set WWW-Authenticate header for MCP OAuth errors (RFC 9728)
|
|
52
|
+
const mcpException = exception instanceof HttpException ? exception : null;
|
|
53
|
+
if (mcpException?.wwwAuthenticate) {
|
|
54
|
+
response.setHeader('WWW-Authenticate', mcpException.wwwAuthenticate);
|
|
55
|
+
response.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');
|
|
56
|
+
}
|
|
51
57
|
response.status(status).json(errorResponse);
|
|
52
58
|
}
|
|
53
59
|
constructor(){
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/common/filters/all-exceptions.filter.ts"],"sourcesContent":["/**\n * Global Exception Filter\n *\n * Catches all exceptions and formats them consistently.\n */\n\nimport {\n ArgumentsHost,\n Catch,\n ExceptionFilter,\n HttpException,\n HttpStatus,\n Logger,\n} from '@nestjs/common';\nimport { Request, Response } from 'express';\n\ninterface ErrorResponse {\n statusCode: number;\n message: string;\n error: string;\n timestamp: string;\n path: string;\n requestId?: string;\n}\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n private readonly logger = new Logger(AllExceptionsFilter.name);\n\n catch(exception: unknown, host: ArgumentsHost): void {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse<Response>();\n const request = ctx.getRequest<Request>();\n\n let status = HttpStatus.INTERNAL_SERVER_ERROR;\n let message = 'Internal server error';\n let error = 'Internal Server Error';\n\n if (exception instanceof HttpException) {\n status = exception.getStatus();\n const exceptionResponse = exception.getResponse();\n\n if (typeof exceptionResponse === 'string') {\n message = exceptionResponse;\n } else if (typeof exceptionResponse === 'object') {\n const responseObj = exceptionResponse as Record<string, unknown>;\n message = (responseObj.message as string) || message;\n error = (responseObj.error as string) || exception.name;\n }\n } else if (exception instanceof Error) {\n message = exception.message;\n error = exception.name;\n }\n\n // Log error\n this.logger.error(`${request.method} ${request.url} - ${status} - ${message}`, {\n exception: exception instanceof Error ? exception.stack : String(exception),\n requestId: request.headers['x-request-id'],\n });\n\n const errorResponse: ErrorResponse = {\n statusCode: status,\n message,\n error,\n timestamp: new Date().toISOString(),\n path: request.url,\n };\n\n // Add request ID if present\n const requestId = request.headers['x-request-id'];\n if (typeof requestId === 'string') {\n errorResponse.requestId = requestId;\n }\n\n response.status(status).json(errorResponse);\n }\n}\n"],"names":["Catch","HttpException","HttpStatus","Logger","AllExceptionsFilter","catch","exception","host","ctx","switchToHttp","response","getResponse","request","getRequest","status","INTERNAL_SERVER_ERROR","message","error","getStatus","exceptionResponse","responseObj","name","Error","logger","method","url","stack","String","requestId","headers","errorResponse","statusCode","timestamp","Date","toISOString","path","json"],"mappings":";;;;;;AAAA;;;;CAIC,GAED,SAEEA,KAAK,EAELC,aAAa,EACbC,UAAU,EACVC,MAAM,QACD,iBAAiB;
|
|
1
|
+
{"version":3,"sources":["../../../src/common/filters/all-exceptions.filter.ts"],"sourcesContent":["/**\n * Global Exception Filter\n *\n * Catches all exceptions and formats them consistently.\n */\n\nimport {\n ArgumentsHost,\n Catch,\n ExceptionFilter,\n HttpException,\n HttpStatus,\n Logger,\n} from '@nestjs/common';\nimport { Request, Response } from 'express';\n\ninterface ErrorResponse {\n statusCode: number;\n message: string;\n error: string;\n timestamp: string;\n path: string;\n requestId?: string;\n}\n\ntype McpHttpException = HttpException & {\n wwwAuthenticate?: string;\n};\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n private readonly logger = new Logger(AllExceptionsFilter.name);\n\n catch(exception: unknown, host: ArgumentsHost): void {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse<Response>();\n const request = ctx.getRequest<Request>();\n\n let status = HttpStatus.INTERNAL_SERVER_ERROR;\n let message = 'Internal server error';\n let error = 'Internal Server Error';\n\n if (exception instanceof HttpException) {\n status = exception.getStatus();\n const exceptionResponse = exception.getResponse();\n\n if (typeof exceptionResponse === 'string') {\n message = exceptionResponse;\n } else if (typeof exceptionResponse === 'object') {\n const responseObj = exceptionResponse as Record<string, unknown>;\n message = (responseObj.message as string) || message;\n error = (responseObj.error as string) || exception.name;\n }\n } else if (exception instanceof Error) {\n message = exception.message;\n error = exception.name;\n }\n\n // Log error\n this.logger.error(`${request.method} ${request.url} - ${status} - ${message}`, {\n exception: exception instanceof Error ? exception.stack : String(exception),\n requestId: request.headers['x-request-id'],\n });\n\n const errorResponse: ErrorResponse = {\n statusCode: status,\n message,\n error,\n timestamp: new Date().toISOString(),\n path: request.url,\n };\n\n // Add request ID if present\n const requestId = request.headers['x-request-id'];\n if (typeof requestId === 'string') {\n errorResponse.requestId = requestId;\n }\n\n // Set WWW-Authenticate header for MCP OAuth errors (RFC 9728)\n const mcpException = exception instanceof HttpException ? (exception as McpHttpException) : null;\n if (mcpException?.wwwAuthenticate) {\n response.setHeader('WWW-Authenticate', mcpException.wwwAuthenticate);\n response.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');\n }\n\n response.status(status).json(errorResponse);\n }\n}\n"],"names":["Catch","HttpException","HttpStatus","Logger","AllExceptionsFilter","catch","exception","host","ctx","switchToHttp","response","getResponse","request","getRequest","status","INTERNAL_SERVER_ERROR","message","error","getStatus","exceptionResponse","responseObj","name","Error","logger","method","url","stack","String","requestId","headers","errorResponse","statusCode","timestamp","Date","toISOString","path","mcpException","wwwAuthenticate","setHeader","json"],"mappings":";;;;;;AAAA;;;;CAIC,GAED,SAEEA,KAAK,EAELC,aAAa,EACbC,UAAU,EACVC,MAAM,QACD,iBAAiB;AAiBxB,OAAO,MAAMC;IAGXC,MAAMC,SAAkB,EAAEC,IAAmB,EAAQ;QACnD,MAAMC,MAAMD,KAAKE,YAAY;QAC7B,MAAMC,WAAWF,IAAIG,WAAW;QAChC,MAAMC,UAAUJ,IAAIK,UAAU;QAE9B,IAAIC,SAASZ,WAAWa,qBAAqB;QAC7C,IAAIC,UAAU;QACd,IAAIC,QAAQ;QAEZ,IAAIX,qBAAqBL,eAAe;YACtCa,SAASR,UAAUY,SAAS;YAC5B,MAAMC,oBAAoBb,UAAUK,WAAW;YAE/C,IAAI,OAAOQ,sBAAsB,UAAU;gBACzCH,UAAUG;YACZ,OAAO,IAAI,OAAOA,sBAAsB,UAAU;gBAChD,MAAMC,cAAcD;gBACpBH,UAAU,AAACI,YAAYJ,OAAO,IAAeA;gBAC7CC,QAAQ,AAACG,YAAYH,KAAK,IAAeX,UAAUe,IAAI;YACzD;QACF,OAAO,IAAIf,qBAAqBgB,OAAO;YACrCN,UAAUV,UAAUU,OAAO;YAC3BC,QAAQX,UAAUe,IAAI;QACxB;QAEA,YAAY;QACZ,IAAI,CAACE,MAAM,CAACN,KAAK,CAAC,GAAGL,QAAQY,MAAM,CAAC,CAAC,EAAEZ,QAAQa,GAAG,CAAC,GAAG,EAAEX,OAAO,GAAG,EAAEE,SAAS,EAAE;YAC7EV,WAAWA,qBAAqBgB,QAAQhB,UAAUoB,KAAK,GAAGC,OAAOrB;YACjEsB,WAAWhB,QAAQiB,OAAO,CAAC,eAAe;QAC5C;QAEA,MAAMC,gBAA+B;YACnCC,YAAYjB;YACZE;YACAC;YACAe,WAAW,IAAIC,OAAOC,WAAW;YACjCC,MAAMvB,QAAQa,GAAG;QACnB;QAEA,4BAA4B;QAC5B,MAAMG,YAAYhB,QAAQiB,OAAO,CAAC,eAAe;QACjD,IAAI,OAAOD,cAAc,UAAU;YACjCE,cAAcF,SAAS,GAAGA;QAC5B;QAEA,8DAA8D;QAC9D,MAAMQ,eAAe9B,qBAAqBL,gBAAiBK,YAAiC;QAC5F,IAAI8B,cAAcC,iBAAiB;YACjC3B,SAAS4B,SAAS,CAAC,oBAAoBF,aAAaC,eAAe;YACnE3B,SAAS4B,SAAS,CAAC,iCAAiC;QACtD;QAEA5B,SAASI,MAAM,CAACA,QAAQyB,IAAI,CAACT;IAC/B;;aAvDiBP,SAAS,IAAIpB,OAAOC,oBAAoBiB,IAAI;;AAwD/D"}
|