diff --git a/backend/src/index.ts b/backend/src/index.ts index 1aa09ae3..aa573bb0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -59,6 +59,7 @@ import { AGENT_API_V1_BASE, AGENT_API_V1_LLM_BASE, LEGACY_AGENT_API_BASE, + markLegacyApi, rejectLegacyAgentApi, } from './middleware/legacyAgentApi'; @@ -149,8 +150,22 @@ app.get('/debug', (req, res) => { app.use('/api/sql', sqlRoutes); app.use('/api/auth', enterpriseAuthRoutes); app.use('/api/auth', enterpriseApiKeyRoutes); -app.use('/api/traces', simpleTraceRoutes); -app.use(AGENT_API_V1_LLM_BASE, aiChatRoutes); +app.use( + '/api/traces', + markLegacyApi( + '/api/workspaces/:workspaceId/traces', + 'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs', + ), + simpleTraceRoutes, +); +app.use( + AGENT_API_V1_LLM_BASE, + markLegacyApi( + '/api/workspaces/:workspaceId/agent/llm', + 'Legacy agent LLM API is deprecated. Migrate to workspace-scoped agent APIs', + ), + aiChatRoutes, +); app.use('/api/perfetto', perfettoLocalRoutes); app.use('/api/auto-analysis', autoAnalysisRoutes); app.use('/api/sessions', sessionRoutes); @@ -160,10 +175,31 @@ app.use('/api/template-analysis', templateAnalysisRoutes); app.use('/api/skills', skillRoutes); app.use('/api/admin', skillAdminRoutes); app.use('/api/admin', strategyAdminRoutes); -app.use('/api/reports', reportRoutes); -app.use(AGENT_API_V1_BASE, agentRoutes); +app.use( + '/api/reports', + markLegacyApi( + '/api/workspaces/:workspaceId/reports', + 'Legacy report API is deprecated. Migrate to workspace-scoped report APIs', + ), + reportRoutes, +); +app.use( + AGENT_API_V1_BASE, + markLegacyApi( + '/api/workspaces/:workspaceId/agent', + 'Legacy agent API is deprecated. Migrate to workspace-scoped agent APIs', + ), + agentRoutes, +); app.use('/api/advanced-ai', advancedAIRoutes); -app.use('/api/v1/providers', providerRoutes); +app.use( + '/api/v1/providers', + markLegacyApi( + '/api/workspaces/:workspaceId/providers', + 'Legacy provider API is deprecated. Migrate to workspace-scoped provider APIs', + ), + providerRoutes, +); app.use('/api/flamegraph', flamegraphRoutes); app.use('/api/critical-path', criticalPathRoutes); app.use('/api/baselines', baselineRoutes); diff --git a/backend/src/middleware/__tests__/legacyApiCompatibility.test.ts b/backend/src/middleware/__tests__/legacyApiCompatibility.test.ts new file mode 100644 index 00000000..5cec3e02 --- /dev/null +++ b/backend/src/middleware/__tests__/legacyApiCompatibility.test.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) +// This file is part of SmartPerfetto. See LICENSE for details. + +import express from 'express'; +import request from 'supertest'; +import { + getLegacyApiUsageSnapshot, + resetLegacyApiUsageTelemetryForTests, +} from '../../services/legacyApiTelemetry'; +import { LEGACY_AGENT_API_SUNSET, markLegacyApi } from '../legacyAgentApi'; + +describe('legacy API compatibility headers', () => { + afterEach(() => { + resetLegacyApiUsageTelemetryForTests(); + }); + + test('adds deprecation headers and records telemetry before delegating to current handlers', async () => { + const app = express(); + app.get( + '/api/traces', + markLegacyApi( + '/api/workspaces/:workspaceId/traces', + 'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs', + ), + (_req, res) => res.json({ success: true }), + ); + + const res = await request(app) + .get('/api/traces') + .set('Authorization', 'Bearer test-token') + .expect(200); + + expect(res.headers.deprecation).toBe('true'); + expect(res.headers.sunset).toBe(LEGACY_AGENT_API_SUNSET); + expect(res.headers.link).toBe( + '; rel="successor-version"', + ); + expect(res.headers.warning).toContain('Legacy trace API is deprecated'); + expect(res.body).toEqual({ success: true }); + + const telemetry = getLegacyApiUsageSnapshot(); + expect(telemetry.totalLegacyRequests).toBe(1); + expect(telemetry.topPaths[0].key).toBe('GET /api/traces'); + }); +}); diff --git a/backend/src/middleware/legacyAgentApi.ts b/backend/src/middleware/legacyAgentApi.ts index 0904c02c..d9f8d825 100644 --- a/backend/src/middleware/legacyAgentApi.ts +++ b/backend/src/middleware/legacyAgentApi.ts @@ -11,6 +11,17 @@ export const LEGACY_AGENT_API_BASE = '/api/agent'; export const LEGACY_AGENT_API_LLM_BASE = '/api/agent/llm'; export const LEGACY_AGENT_API_SUNSET = 'Wed, 30 Jun 2027 00:00:00 GMT'; +export function markLegacyApi(successor: string, message: string) { + return (req: Request, res: Response, next: NextFunction): void => { + recordLegacyApiUsage(req); + res.setHeader('Deprecation', 'true'); + res.setHeader('Sunset', LEGACY_AGENT_API_SUNSET); + res.setHeader('Link', `<${successor}>; rel="successor-version"`); + res.setHeader('Warning', `299 - "${message}"`); + next(); + }; +} + export function markLegacyAgentApi(req: Request, res: Response, next: NextFunction): void { recordLegacyApiUsage(req); res.setHeader('Deprecation', 'true'); diff --git a/backend/src/routes/__tests__/requestContextRouteCoverage.test.ts b/backend/src/routes/__tests__/requestContextRouteCoverage.test.ts index 90b73b72..b9437bea 100644 --- a/backend/src/routes/__tests__/requestContextRouteCoverage.test.ts +++ b/backend/src/routes/__tests__/requestContextRouteCoverage.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from '@jest/globals'; import express from 'express'; import request from 'supertest'; +import { markLegacyApi } from '../../middleware/legacyAgentApi'; import reportRoutes from '../reportRoutes'; import traceRoutes from '../simpleTraceRoutes'; @@ -13,8 +14,22 @@ const originalApiKey = process.env.SMARTPERFETTO_API_KEY; function makeApp(): express.Express { const app = express(); app.use(express.json()); - app.use('/api/traces', traceRoutes); - app.use('/api/reports', reportRoutes); + app.use( + '/api/traces', + markLegacyApi( + '/api/workspaces/:workspaceId/traces', + 'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs', + ), + traceRoutes, + ); + app.use( + '/api/reports', + markLegacyApi( + '/api/workspaces/:workspaceId/reports', + 'Legacy report API is deprecated. Migrate to workspace-scoped report APIs', + ), + reportRoutes, + ); return app; } @@ -51,6 +66,8 @@ describe('RequestContext route coverage', () => { const res = await request(makeApp()).get('/api/traces'); expect(res.status).toBe(401); + expect(res.headers.deprecation).toBe('true'); + expect(res.headers.sunset).toBeDefined(); expect(res.body.error).toBe('Unauthorized'); }); @@ -60,6 +77,8 @@ describe('RequestContext route coverage', () => { const res = await request(makeApp()).get('/api/reports/missing-report'); expect(res.status).toBe(401); + expect(res.headers.deprecation).toBe('true'); + expect(res.headers.sunset).toBeDefined(); expect(res.body.error).toBe('Unauthorized'); }); }); diff --git a/docs/features/enterprise-multi-tenant/README.md b/docs/features/enterprise-multi-tenant/README.md index 5fa3f991..60c8a47f 100644 --- a/docs/features/enterprise-multi-tenant/README.md +++ b/docs/features/enterprise-multi-tenant/README.md @@ -33,7 +33,7 @@ - [x] 2.2 OIDC SSO 集成 + Onboarding flow(§15 全流程,含 audit) - [x] 2.3 API key 管理(创建 / 撤销 / scope / 过期 / 审计) - [x] 2.4 Membership / Role / RBAC 权限矩阵(§8.2)+ owner guard 全 route 覆盖 -- [ ] 2.5 旧 API 兼容 wrapper:返回 `Deprecation: true` + `Sunset` header;统一走 RequestContext +- [x] 2.5 旧 API 兼容 wrapper:返回 `Deprecation: true` + `Sunset` header;统一走 RequestContext - [ ] 2.6 Resource-oriented API 切到 `/api/workspaces/:workspaceId/*`(§8.3) - [ ] 2.7 前端 workspace selection UI + workspace/window 上下文持久化分层(§9.2 表格) - [ ] 2.8 单元测试覆盖:RequestContext 解析 / RBAC / owner guard / 旧路径包装