| 1 | import { describe, expect, it } from 'vitest';
|
| 2 | import { POST as mcpHandler } from '../api/mcp.js';
|
| 3 | import { GET as metadataHandler } from '../api/oauth-protected-resource.js';
|
| 4 | import { dashboardTokenFromCookie } from '../apps/api/src/auth0.js';
|
| 5 |
|
| 6 | describe('hosted MCP authorization', () => {
|
| 7 | it('reads only the named dashboard session cookie', () => {
|
| 8 | expect(dashboardTokenFromCookie('other=x; openclasp_session=token%2Evalue; last=y')).toBe(
|
| 9 | 'token.value',
|
| 10 | );
|
| 11 | expect(dashboardTokenFromCookie('other=x')).toBeUndefined();
|
| 12 | });
|
| 13 | it('challenges unauthenticated remote MCP requests', async () => {
|
| 14 | const response = await mcpHandler(
|
| 15 | new Request('https://openclasp.example/mcp', {
|
| 16 | method: 'POST',
|
| 17 | headers: { 'content-type': 'application/json' },
|
| 18 | body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
|
| 19 | }),
|
| 20 | );
|
| 21 | expect(response.status).toBe(401);
|
| 22 | expect(response.headers.get('www-authenticate')).toBe(
|
| 23 | 'Bearer resource_metadata="https://openclasp.example/.well-known/oauth-protected-resource/mcp", scope="mcp:access profile:read interaction:write feedback:write agent:manage"',
|
| 24 | );
|
| 25 | expect(response.headers.get('www-authenticate')).not.toContain('invalid_token');
|
| 26 | expect(response.headers.get('cache-control')).toBe('no-store');
|
| 27 | expect(await response.text()).toBe('');
|
| 28 | });
|
| 29 |
|
| 30 | it('publishes Auth0 protected-resource metadata', async () => {
|
| 31 | const response = metadataHandler(
|
| 32 | new Request('https://openclasp.example/.well-known/oauth-protected-resource'),
|
| 33 | );
|
| 34 | expect(response.status).toBe(200);
|
| 35 | await expect(response.json()).resolves.toMatchObject({
|
| 36 | resource: 'https://openclasp.example/mcp',
|
| 37 | authorization_servers: ['https://openclasp.example'],
|
| 38 | scopes_supported: expect.arrayContaining([
|
| 39 | 'mcp:access',
|
| 40 | 'profile:read',
|
| 41 | 'interaction:write',
|
| 42 | 'feedback:write',
|
| 43 | 'agent:manage',
|
| 44 | ]),
|
| 45 | });
|
| 46 | });
|
| 47 |
|
| 48 | it('keeps protected-resource metadata bound to the requested host during domain migration', async () => {
|
| 49 | const configuredUrl = process.env.OPENCLASP_MCP_URL;
|
| 50 | process.env.OPENCLASP_MCP_URL = 'https://openclasp.dev/mcp';
|
| 51 | try {
|
| 52 | const legacyResponse = metadataHandler(
|
| 53 | new Request('https://openclasp.vercel.app/.well-known/oauth-protected-resource/mcp'),
|
| 54 | );
|
| 55 | await expect(legacyResponse.json()).resolves.toMatchObject({
|
| 56 | resource: 'https://openclasp.vercel.app/mcp',
|
| 57 | authorization_servers: ['https://openclasp.vercel.app'],
|
| 58 | });
|
| 59 | } finally {
|
| 60 | if (configuredUrl === undefined) delete process.env.OPENCLASP_MCP_URL;
|
| 61 | else process.env.OPENCLASP_MCP_URL = configuredUrl;
|
| 62 | }
|
| 63 | });
|
| 64 | });
|