mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-04 15:53:07 +08:00
- Added CrossCliSyncPanel component for synchronizing MCP servers between Claude and Codex. - Implemented server selection, copy operations, and result handling. - Added tests for path mapping on Windows drives. - Created E2E tests for ask_question Answer Broker functionality. - Introduced MCP Tools Test Script for validating modified read_file and edit_file tools. - Updated path_mapper to ensure correct drive formatting on Windows. - Added .gitignore for ace-tool directory.
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
import type http from 'http';
|
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
import type { TokenManager } from './token-manager.js';
|
|
|
|
export interface AuthMiddlewareContext {
|
|
pathname: string;
|
|
req: IncomingMessage;
|
|
res: ServerResponse;
|
|
tokenManager: TokenManager;
|
|
secretKey: string;
|
|
unauthenticatedPaths?: Set<string>;
|
|
}
|
|
|
|
function parseCookieHeader(cookieHeader: string | null | undefined): Record<string, string> {
|
|
if (!cookieHeader) return {};
|
|
|
|
const cookies: Record<string, string> = {};
|
|
for (const part of cookieHeader.split(';')) {
|
|
const [rawName, ...rawValueParts] = part.trim().split('=');
|
|
if (!rawName) continue;
|
|
const rawValue = rawValueParts.join('=');
|
|
try {
|
|
cookies[rawName] = decodeURIComponent(rawValue);
|
|
} catch {
|
|
cookies[rawName] = rawValue;
|
|
}
|
|
}
|
|
return cookies;
|
|
}
|
|
|
|
function getHeaderValue(header: string | string[] | undefined): string | null {
|
|
if (!header) return null;
|
|
if (Array.isArray(header)) return header[0] ?? null;
|
|
return header;
|
|
}
|
|
|
|
export function extractAuthToken(req: IncomingMessage): string | null {
|
|
const authorization = getHeaderValue(req.headers.authorization);
|
|
if (authorization) {
|
|
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
if (match?.[1]) return match[1].trim();
|
|
}
|
|
|
|
const cookies = parseCookieHeader(getHeaderValue(req.headers.cookie));
|
|
if (cookies.auth_token) return cookies.auth_token;
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isLocalhostRequest(req: IncomingMessage): boolean {
|
|
const remote = req.socket?.remoteAddress ?? '';
|
|
return remote === '127.0.0.1' || remote === '::1' || remote === '::ffff:127.0.0.1';
|
|
}
|
|
|
|
export function setAuthCookie(res: ServerResponse, token: string, expiresAt: Date): void {
|
|
const maxAgeSeconds = Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000));
|
|
|
|
const attributes = [
|
|
`auth_token=${encodeURIComponent(token)}`,
|
|
'Path=/',
|
|
'HttpOnly',
|
|
'SameSite=Strict',
|
|
`Max-Age=${maxAgeSeconds}`,
|
|
];
|
|
|
|
res.setHeader('Set-Cookie', attributes.join('; '));
|
|
}
|
|
|
|
function writeJson(res: ServerResponse, status: number, body: Record<string, unknown>): void {
|
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
/**
|
|
* Public API endpoints that can be accessed from localhost without authentication
|
|
* These are read-only endpoints used by the dashboard for data fetching
|
|
*/
|
|
const LOCALHOST_PUBLIC_PATHS = [
|
|
'/api/data',
|
|
'/api/orchestrator/flows',
|
|
'/api/orchestrator/templates',
|
|
'/api/orchestrator/executions',
|
|
'/api/orchestrator/templates/remote',
|
|
'/api/mcp-config',
|
|
'/api/ccw/tools',
|
|
'/api/ccw/installations',
|
|
'/api/cli/endpoints',
|
|
'/api/skills',
|
|
'/api/providers',
|
|
'/api/litellm-api/providers',
|
|
'/api/litellm-api/endpoints',
|
|
'/api/health',
|
|
'/api/a2ui/answer',
|
|
];
|
|
|
|
/**
|
|
* Check if a path is a public API endpoint (accessible from localhost without auth)
|
|
*/
|
|
function isLocalPublicPath(pathname: string): boolean {
|
|
// Exact match
|
|
if (LOCALHOST_PUBLIC_PATHS.includes(pathname)) return true;
|
|
|
|
// Prefix match for paths with parameters (e.g., /api/orchestrator/flows/:id)
|
|
for (const publicPath of LOCALHOST_PUBLIC_PATHS) {
|
|
if (pathname.startsWith(publicPath + '/') || pathname.startsWith(publicPath.replace(/\/[^/]*$/, '/'))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Special handling for paths with wildcards
|
|
if (pathname.startsWith('/api/orchestrator/flows/')) return true;
|
|
if (pathname.startsWith('/api/orchestrator/executions/')) return true;
|
|
if (pathname.startsWith('/api/orchestrator/templates/')) return true;
|
|
if (pathname.startsWith('/api/litellm-api/providers/')) return true;
|
|
if (pathname.startsWith('/api/litellm-api/endpoints/')) return true;
|
|
if (pathname.startsWith('/api/litellm-api/models/')) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
export function authMiddleware(ctx: AuthMiddlewareContext): boolean {
|
|
const { pathname, req, res, tokenManager, secretKey, unauthenticatedPaths } = ctx;
|
|
|
|
if (!pathname.startsWith('/api/')) return true;
|
|
if (unauthenticatedPaths?.has(pathname)) return true;
|
|
|
|
// Allow localhost requests to public API endpoints without authentication
|
|
// This enables the Vite dev server (localhost:5173) to proxy API requests
|
|
if (isLocalhostRequest(req) && isLocalPublicPath(pathname)) {
|
|
(req as http.IncomingMessage & { authenticated?: boolean }).authenticated = true;
|
|
return true;
|
|
}
|
|
|
|
const token = extractAuthToken(req);
|
|
if (!token) {
|
|
writeJson(res, 401, { error: 'Unauthorized' });
|
|
return false;
|
|
}
|
|
|
|
const ok = tokenManager.validateToken(token, secretKey);
|
|
if (!ok) {
|
|
writeJson(res, 401, { error: 'Unauthorized' });
|
|
return false;
|
|
}
|
|
|
|
(req as http.IncomingMessage & { authenticated?: boolean }).authenticated = true;
|
|
return true;
|
|
}
|