@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.
Files changed (56) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +52 -0
  3. package/dist/__tests__/integration/mcp-proxy-auth-http.test.js +283 -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 +171 -110
  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 +63 -2
  14. package/dist/main.js.map +1 -1
  15. package/dist/modules/auth/auth.config.js +10 -5
  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 +95 -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/health/health.controller.js +1 -1
  26. package/dist/modules/health/health.controller.js.map +1 -1
  27. package/dist/modules/mcp/mcp.service.js +48 -8
  28. package/dist/modules/mcp/mcp.service.js.map +1 -1
  29. package/dist/modules/oauth/oauth.controller.js +78 -1
  30. package/dist/modules/oauth/oauth.controller.js.map +1 -1
  31. package/dist/modules/oauth/oauth.service.js +197 -1
  32. package/dist/modules/oauth/oauth.service.js.map +1 -1
  33. package/dist/modules/proxy/proxy.controller.js +152 -27
  34. package/dist/modules/proxy/proxy.controller.js.map +1 -1
  35. package/dist/modules/proxy/proxy.service.js +28 -4
  36. package/dist/modules/proxy/proxy.service.js.map +1 -1
  37. package/docker-entrypoint.sh +15 -2
  38. package/package.json +9 -7
  39. package/src/__tests__/integration/mcp-proxy-auth-http.test.ts +311 -0
  40. package/src/__tests__/integration/oauth-authorize-callback.test.ts +155 -0
  41. package/src/__tests__/integration/proxy-auth.test.ts +151 -168
  42. package/src/__tests__/unit/auth.guard.test.ts +12 -2
  43. package/src/common/filters/all-exceptions.filter.ts +11 -0
  44. package/src/main.ts +56 -2
  45. package/src/modules/auth/auth.config.ts +9 -4
  46. package/src/modules/auth/auth.module.ts +3 -2
  47. package/src/modules/auth/auth.service.ts +2 -2
  48. package/src/modules/auth/mcp-oauth.guard.ts +102 -0
  49. package/src/modules/auth/mcp-oauth.utils.ts +80 -0
  50. package/src/modules/health/health.controller.ts +1 -1
  51. package/src/modules/mcp/mcp.service.ts +54 -12
  52. package/src/modules/oauth/oauth.controller.ts +84 -1
  53. package/src/modules/oauth/oauth.service.ts +218 -1
  54. package/src/modules/proxy/proxy.controller.ts +120 -25
  55. package/src/modules/proxy/proxy.service.ts +26 -4
  56. package/vitest.config.ts +2 -1
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Integration Tests: Proxy controller auth
3
3
  *
4
- * Verifies that:
5
- * - Proxy routes are @Public() (no session cookie required)
6
- * - Bearer token validation via resolveUser flow
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
- * - Unauthenticated access uses __unauthenticated__ sentinel
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
- on: vi.fn()
92
+ query,
93
+ on: vi.fn(),
94
+ user: undefined
79
95
  };
80
96
  }
81
- describe('Proxy controller auth', ()=>{
82
- let controller;
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
- let proxyService;
85
- let settingsService;
86
- let eventEmitter;
87
- // ────────────────────────────────────────────────
88
- // Unauthenticated access (no Bearer token)
89
- // ────────────────────────────────────────────────
90
- describe('unauthenticated access (no Bearer token)', ()=>{
91
- beforeEach(()=>{
92
- authService = createMockAuthService();
93
- proxyService = createMockProxyService();
94
- settingsService = createMockSettingsService();
95
- eventEmitter = createMockEventEmitter();
96
- controller = new ProxyController(proxyService, settingsService, eventEmitter, authService);
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
- it('org-scoped POST request passes __unauthenticated__ to proxy service', async ()=>{
99
- const req = createMockRequest();
100
- const mcpRequest = {
101
- jsonrpc: '2.0',
102
- id: 1,
103
- method: 'tools/list'
104
- };
105
- await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);
106
- expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', mcpRequest, '__unauthenticated__');
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
- it('gateway POST request passes __unauthenticated__', async ()=>{
109
- const req = createMockRequest();
110
- const mcpRequest = {
111
- jsonrpc: '2.0',
112
- id: 1,
113
- method: 'initialize'
114
- };
115
- await controller.handleGatewayRequest(req, mcpRequest);
116
- expect(proxyService.handleRequest).toHaveBeenCalledWith('default', mcpRequest, '__unauthenticated__');
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
- it('non-Bearer Authorization header falls back to __unauthenticated__', async ()=>{
119
- const req = createMockRequest({
120
- authorization: 'Basic dXNlcjpwYXNz'
121
- });
122
- const mcpRequest = {
123
- jsonrpc: '2.0',
124
- id: 1,
125
- method: 'tools/list'
126
- };
127
- await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);
128
- expect(authService.validateMcpToken).not.toHaveBeenCalled();
129
- expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', mcpRequest, '__unauthenticated__');
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
- // Authenticated access with Bearer tokens
134
- // ────────────────────────────────────────────────
135
- describe('authenticated access with Bearer tokens', ()=>{
136
- const validUser = {
137
- id: 'user-1',
138
- name: 'Token User',
139
- email: 'token@example.com'
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
- beforeEach(()=>{
142
- authService = createMockAuthService();
143
- proxyService = createMockProxyService();
144
- settingsService = createMockSettingsService();
145
- eventEmitter = createMockEventEmitter();
146
- controller = new ProxyController(proxyService, settingsService, eventEmitter, authService);
209
+ authService.validateMcpToken.mockResolvedValue(null);
210
+ authService.getSession.mockResolvedValue({
211
+ user,
212
+ session: {
213
+ id: 's1',
214
+ userId: user.id
215
+ }
147
216
  });
148
- it('valid Bearer token resolves to authenticated user for org-scoped request', async ()=>{
149
- authService.validateMcpToken.mockResolvedValue(validUser);
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
- it('invalid Bearer token throws UnauthorizedException', async ()=>{
163
- authService.validateMcpToken.mockResolvedValue(null);
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 invalid-token'
247
+ authorization: 'Bearer some-token'
166
248
  });
167
- const mcpRequest = {
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(authService.validateMcpToken).not.toHaveBeenCalled();
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: null
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: null };\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"],"names":["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,qBAAqB,QAAQ,iBAAiB;AAEzE,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;QAAK;QACjFvB,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;AACF"}
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;AAaxB,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;QAEAlB,SAASI,MAAM,CAACA,QAAQsB,IAAI,CAACN;IAC/B;;aAhDiBP,SAAS,IAAIpB,OAAOC,oBAAoBiB,IAAI;;AAiD/D"}
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"}