mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 13:38:43 +02:00
- Create mcpTokenStore: in-memory token storage keyed by normalized server URL with automatic expiry checking - Create mcpOAuthService: full OAuth 2.1 + PKCE flow implementation - Protected Resource Metadata discovery (RFC 9728) - Authorization Server Metadata fetch (RFC 8414) - Dynamic Client Registration (RFC 7591) when no static client ID - PKCE S256 code challenge generation - Local HTTP callback server for auth code receipt - Browser-based consent via Electron shell.openExternal - Authorization code to token exchange - Add startSessionMcpAuth IPC channel and handler to trigger OAuth flow - Inject stored OAuth tokens as Authorization headers in buildRunTurnToolingConfig - Update McpAuthBanner with 'Authenticate in browser' button and loading state - Add tests for token store (7 tests) and token injection (3 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
|
|
import {
|
|
getStoredToken,
|
|
storeToken,
|
|
clearToken,
|
|
clearAllTokens,
|
|
type McpOAuthToken,
|
|
} from '@main/services/mcpTokenStore';
|
|
|
|
describe('MCP OAuth token store', () => {
|
|
beforeEach(() => {
|
|
clearAllTokens();
|
|
});
|
|
|
|
test('stores and retrieves tokens by server URL', () => {
|
|
const token: McpOAuthToken = {
|
|
accessToken: 'abc123',
|
|
tokenType: 'Bearer',
|
|
};
|
|
|
|
storeToken('https://mcp.example.com/api', token);
|
|
|
|
expect(getStoredToken('https://mcp.example.com/api')).toEqual(token);
|
|
});
|
|
|
|
test('normalizes trailing slashes and case in server URLs', () => {
|
|
const token: McpOAuthToken = {
|
|
accessToken: 'xyz',
|
|
tokenType: 'Bearer',
|
|
};
|
|
|
|
storeToken('https://MCP.Example.com/api/', token);
|
|
|
|
expect(getStoredToken('https://mcp.example.com/api')).toEqual(token);
|
|
expect(getStoredToken('https://mcp.example.com/api/')).toEqual(token);
|
|
});
|
|
|
|
test('returns undefined for unknown server URLs', () => {
|
|
expect(getStoredToken('https://unknown.example.com')).toBeUndefined();
|
|
});
|
|
|
|
test('clears a token for a specific server', () => {
|
|
storeToken('https://a.example.com', { accessToken: 'a', tokenType: 'Bearer' });
|
|
storeToken('https://b.example.com', { accessToken: 'b', tokenType: 'Bearer' });
|
|
|
|
clearToken('https://a.example.com');
|
|
|
|
expect(getStoredToken('https://a.example.com')).toBeUndefined();
|
|
expect(getStoredToken('https://b.example.com')).toBeDefined();
|
|
});
|
|
|
|
test('clears all stored tokens', () => {
|
|
storeToken('https://a.example.com', { accessToken: 'a', tokenType: 'Bearer' });
|
|
storeToken('https://b.example.com', { accessToken: 'b', tokenType: 'Bearer' });
|
|
|
|
clearAllTokens();
|
|
|
|
expect(getStoredToken('https://a.example.com')).toBeUndefined();
|
|
expect(getStoredToken('https://b.example.com')).toBeUndefined();
|
|
});
|
|
|
|
test('returns undefined for expired tokens', () => {
|
|
const token: McpOAuthToken = {
|
|
accessToken: 'expired',
|
|
tokenType: 'Bearer',
|
|
expiresAt: Date.now() - 1_000,
|
|
};
|
|
|
|
storeToken('https://mcp.example.com', token);
|
|
|
|
expect(getStoredToken('https://mcp.example.com')).toBeUndefined();
|
|
});
|
|
|
|
test('returns valid tokens that have not expired', () => {
|
|
const token: McpOAuthToken = {
|
|
accessToken: 'valid',
|
|
tokenType: 'Bearer',
|
|
expiresAt: Date.now() + 60_000,
|
|
};
|
|
|
|
storeToken('https://mcp.example.com', token);
|
|
|
|
expect(getStoredToken('https://mcp.example.com')).toEqual(token);
|
|
});
|
|
});
|