From 42825eafba6c845a32ed00f45fbfe49fe1bae244 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Fri, 22 May 2026 22:16:02 +0100 Subject: [PATCH 1/6] feat(security): add mcp tier (300/min, api-key keyed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP is a distinct interface from the human-facing REST API: server-to- server, always API-key-authenticated, much chattier per session than human-driven REST traffic (LLM agents iterate through tool calls inside a conversation). The api tier's 100/min cap is too tight for legitimate agent workloads and IP keying collapses two customers behind the same NAT'd egress into one bucket. This commit adds the tier; it does not yet wire mcp/route.ts to drop its redundant apiLimiter.check (that lands in the handler-sweep commits later in this PR — the policy rule has to exist first). Source changes: - lib/security/constants.ts: add LIMITS.MCP = envInt('RATE_LIMIT_MCP', 300) - lib/security/rate-limit.ts: add mcpLimiter; extend RateLimitTier union with 'mcp'; add mcp -> mcpLimiter entry to RATE_LIMIT_TIERS registry - lib/security/rate-limit-policy.ts: insert new rule { match: /^\/api\/v1\/mcp(\/|$)/, tier: 'mcp', key: 'api-key' } BEFORE the api consumer block and the catch-all so MCP requests don't fall through to session-user keying (which would defeat the point) Why api-key keying matters: - Two customers sharing a NAT'd egress IP get independent buckets (previously collapsed into one bucket under apiLimiter's IP keying) - One key abused across many IPs is correctly bucketed under a single api-key bucket (previously each IP got its own 100/min bucket) - Per-customer budgets remain tunable via the existing McpRateLimiter per-key sub-cap; the section tier here is the coarse ceiling above Why 300/min: - LLM agents fire rapid tool-call sequences within sessions; 100/min trips on legitimate use, defeating the cap's purpose - 300/min leaves room for normal agent activity while still rate- limiting a runaway loop within ~5 seconds - Override via RATE_LIMIT_MCP for ops who need to tune per deployment Docs: - .context/security/rate-limiting.md: new tier in the policy table (rule #5, renumbers consumer rules); 5-tier section table; explicit paragraph on why mcp is separate from api; updated Configuration table with RATE_LIMIT_MCP row; tier example in "Adding a new tier" section refreshed to show the 5-tier baseline - .env.example: new RATE_LIMIT_MCP=1000 row commented out Tests (+6): - rate-limit.test.ts: mcpLimiter config test (300/min default); RATE_LIMIT_TIERS.mcp identity assertion; full bucket-exhaustion enforcement test mirroring the orchestration tier - rate-limit-policy.test.ts: tier-resolution test for /api/v1/mcp/* paths; boundary test for bare /api/v1/mcp (no trailing slash); declared-order test updated (was 9 rules, now 10); first-match-wins test extended to include mcp ahead of catch-all - constants.test.ts: MCP default test (300); env-var override test; four-way independence test extended to include MCP Coverage: - All 3 modified source files at 100/100/100/100 - Full suite: 850/850 files, 17,915 tests (+6), 6 skipped, 10 todo Co-Authored-By: Claude Opus 4.7 --- .context/security/rate-limiting.md | 22 +++--- .env.example | 1 + lib/security/constants.ts | 12 ++++ lib/security/rate-limit-policy.ts | 15 ++++ lib/security/rate-limit.ts | 26 ++++++- tests/unit/lib/security/constants.test.ts | 54 +++++++++++++-- .../lib/security/rate-limit-policy.test.ts | 68 +++++++++++++++---- tests/unit/lib/security/rate-limit.test.ts | 46 +++++++++++++ 8 files changed, 216 insertions(+), 28 deletions(-) diff --git a/.context/security/rate-limiting.md b/.context/security/rate-limiting.md index 90894d82a..5c2215780 100644 --- a/.context/security/rate-limiting.md +++ b/.context/security/rate-limiting.md @@ -60,29 +60,33 @@ Single source of truth. First match wins; rules are evaluated top to bottom. | 2 | `/api/v1/admin/` | `admin` | `session-user` | 30/min — core admin (users, logs, invitations, flags) | | 3 | `/api/v1/auth/` | `auth` | `ip` | 5/min — app-layer auth endpoints | | 4 | `/api/auth/` | `auth` | `ip` | 5/min — better-auth's own routes | -| 5 | `/api/v1/webhooks/` | `api` | `api-key` | 100/min keyed on `Authorization: Bearer ` | -| 6 | `/api/v1/embed/` | `api` | `embed-token` | 100/min keyed on `X-Embed-Token` header + IP composite | -| 7 | `/api/v1/inbound/` | `api` | `ip` | 100/min — server-to-server (Slack, Postmark, etc.) | -| 8 | `/api/v1/contact` | `api` | `ip` | 100/min — unauthenticated public submission | -| 9 | `/api/v1/` (catch-all) | `api` | `session-user` | 100/min — everything else | +| 5 | `/api/v1/mcp/` | `mcp` | `api-key` | 300/min — LLM-agent transport keyed per api-key | +| 6 | `/api/v1/webhooks/` | `api` | `api-key` | 100/min keyed on `Authorization: Bearer ` | +| 7 | `/api/v1/embed/` | `api` | `embed-token` | 100/min keyed on `X-Embed-Token` header + IP composite | +| 8 | `/api/v1/inbound/` | `api` | `ip` | 100/min — server-to-server (Slack, Postmark, etc.) | +| 9 | `/api/v1/contact` | `api` | `ip` | 100/min — unauthenticated public submission | +| 10 | `/api/v1/` (catch-all) | `api` | `session-user` | 100/min — everything else | -**Order matters.** The orchestration rule (1) must come before the broader admin rule (2) — otherwise `/api/v1/admin/orchestration/agents` would match `/api/v1/admin/` first and land on the tighter 30/min admin tier. The consumer-specific rules (5–8) must come before the catch-all (9) — otherwise webhooks would key on session-user instead of api-key, and so on. +**Order matters.** The orchestration rule (1) must come before the broader admin rule (2) — otherwise `/api/v1/admin/orchestration/agents` would match `/api/v1/admin/` first and land on the tighter 30/min admin tier. The MCP rule (5) and the consumer-specific rules (6–9) must come before the catch-all (10) — otherwise MCP would key on session-user (and fall back to IP, defeating the api-key keying), webhooks would key on session-user, and so on. Tests in `tests/unit/lib/security/rate-limit-policy.test.ts` lock the order in place. ## Section Tiers -Four section tiers, each backed by a single limiter instance in [`RATE_LIMIT_TIERS`](../../lib/security/rate-limit.ts): +Five section tiers, each backed by a single limiter instance in [`RATE_LIMIT_TIERS`](../../lib/security/rate-limit.ts): | Tier | Cap | Env override | Limiter | | --------------- | ------- | ----------------------- | --------------------------- | | `admin` | 30/min | `RATE_LIMIT_ADMIN` | `adminLimiter` | | `orchestration` | 120/min | `RATE_LIMIT_ORCH_ADMIN` | `orchestrationAdminLimiter` | | `api` | 100/min | `RATE_LIMIT_API` | `apiLimiter` | +| `mcp` | 300/min | `RATE_LIMIT_MCP` | `mcpLimiter` | | `auth` | 5/min | (none — security floor) | `authLimiter` | Caps are per-window (1 minute) using the sliding-window algorithm from `lib/security/rate-limit.ts`. Bumps via env vars are intended for development; production should run on the defaults. +**Why `mcp` is separate from `api`.** MCP is a distinct interface — server-to-server, always API-key-authenticated, much chattier per session than human-driven REST traffic (LLM agents iterate through tool calls inside a conversation). The 100/min `api` cap is too tight for legitimate agent workloads; the 300/min default leaves room for normal activity while still rate-limiting a runaway agent loop within ~5 seconds. Per-customer budgets are tunable separately via `McpRateLimiter` against the `apiKey.rateLimit` field; the section tier here is the coarse ceiling above that. + ## Key Strategies How the dispatcher identifies the caller when building the bucket token. Token format: `mw:${tier}:${key}:${identifier}`. @@ -202,6 +206,7 @@ See [`tests/unit/lib/security/rate-limit-middleware.test.ts`](../../tests/unit/l | `RATE_LIMIT_ADMIN` | Override the `admin` tier cap (per-minute) | `30` | | `RATE_LIMIT_ORCH_ADMIN` | Override the `orchestration` tier cap (per-minute) | `120` | | `RATE_LIMIT_API` | Override the `api` tier cap (per-minute) | `100` | +| `RATE_LIMIT_MCP` | Override the `mcp` tier cap (per-minute) | `300` | | `RATE_LIMIT_STORE` | Backing store for the **async** limiter variants only | `memory` | | `REDIS_URL` | Redis connection string (required if `RATE_LIMIT_STORE=redis`) | — | | `RATE_LIMIT_BYPASS` | Test/dev escape hatch — `true` short-circuits the dispatcher | unset | @@ -241,12 +246,13 @@ Three edits, in order: 2. **Extend the registry**: ```typescript - export type RateLimitTier = 'admin' | 'orchestration' | 'api' | 'auth' | 'billing'; + export type RateLimitTier = 'admin' | 'orchestration' | 'api' | 'mcp' | 'auth' | 'billing'; export const RATE_LIMIT_TIERS: Record = { admin: adminLimiter, orchestration: orchestrationAdminLimiter, api: apiLimiter, + mcp: mcpLimiter, auth: authLimiter, billing: billingAdminLimiter, }; diff --git a/.env.example b/.env.example index eb3268ebc..70caafef2 100644 --- a/.env.example +++ b/.env.example @@ -354,6 +354,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000" # RATE_LIMIT_API=1000 # General /api/v1/ catch-all (default 100) # RATE_LIMIT_ADMIN=500 # Core admin endpoints (default 30) # RATE_LIMIT_ORCH_ADMIN=600 # Admin orchestration UI (default 120) +# RATE_LIMIT_MCP=1000 # MCP transport endpoint (default 300, keyed per api-key) # RATE_LIMIT_BYPASS — test/dev escape hatch. When 'true' or '1', the # rate-limit dispatcher short-circuits and returns null for every request. diff --git a/lib/security/constants.ts b/lib/security/constants.ts index c230f883a..3e7c8cfd3 100644 --- a/lib/security/constants.ts +++ b/lib/security/constants.ts @@ -9,6 +9,7 @@ * - `RATE_LIMIT_API` — overrides default 100/min for general API * - `RATE_LIMIT_ADMIN` — overrides default 30/min for core admin endpoints * - `RATE_LIMIT_ORCH_ADMIN` — overrides default 120/min for admin/orchestration endpoints + * - `RATE_LIMIT_MCP` — overrides default 300/min for the MCP transport endpoint */ function envInt(name: string, fallback: number): number { @@ -38,6 +39,17 @@ export const SECURITY_CONSTANTS = { ADMIN: envInt('RATE_LIMIT_ADMIN', 30), /** Admin/orchestration endpoints: 120 requests per minute (override with `RATE_LIMIT_ORCH_ADMIN`) */ ORCH_ADMIN: envInt('RATE_LIMIT_ORCH_ADMIN', 120), + /** + * MCP transport endpoint: 300 requests per minute, keyed per API key + * (override with `RATE_LIMIT_MCP`). + * + * MCP is server-to-server traffic — LLM agents iterating through tool + * calls inside a conversation. The traffic shape is much burstier than + * human-driven API use, so the section cap is higher; per-customer + * budgets are enforced separately by `McpRateLimiter` against the + * `apiKey.rateLimit` field. + */ + MCP: envInt('RATE_LIMIT_MCP', 300), /** Password reset: 3 attempts per 15 minutes */ PASSWORD_RESET: 3, /** Password reset window: 15 minutes */ diff --git a/lib/security/rate-limit-policy.ts b/lib/security/rate-limit-policy.ts index c337720f8..f3dad1e5b 100644 --- a/lib/security/rate-limit-policy.ts +++ b/lib/security/rate-limit-policy.ts @@ -156,6 +156,21 @@ export const RATE_LIMIT_POLICY: readonly RateLimitRule[] = [ skip: skipNonCredentialAuthRoutes, }, + // ── MCP transport (LLM-agent interface) ────────────────────────────────── + // MCP is a distinct interface from the human-facing REST API: server-to- + // server, always API-key-authenticated, much chattier per session (agents + // iterate through tool calls inside a conversation). It gets its own tier + // (300/min by default — override with `RATE_LIMIT_MCP`) keyed by api-key + // so two customers sharing a NAT'd egress get independent buckets. The + // per-customer budget knob is `McpRateLimiter` inside the handler, sized + // from the `apiKey.rateLimit` field; this section tier is the coarse + // ceiling above it. + { + match: /^\/api\/v1\/mcp(\/|$)/, + tier: 'mcp', + key: 'api-key', + }, + // ── Consumer surfaces with non-session keying ──────────────────────────── // These all use the `'api'` tier (100/min) for the section cap, but the // *keying* differs from the default `session-user` because the caller's diff --git a/lib/security/rate-limit.ts b/lib/security/rate-limit.ts index 74ddf8458..2b832e012 100644 --- a/lib/security/rate-limit.ts +++ b/lib/security/rate-limit.ts @@ -252,6 +252,28 @@ export const orchestrationAdminLimiter = createRateLimiter({ uniqueTokenPerInterval: SECURITY_CONSTANTS.RATE_LIMIT.MAX_UNIQUE_TOKENS, }); +/** + * Rate limiter for the MCP transport endpoint (`/api/v1/mcp/*`). + * Limit: 300 requests per minute. Override with `RATE_LIMIT_MCP`. + * + * MCP is a distinct interface from the human-facing REST API. LLM agents + * open sessions and fire rapid tool-call sequences; 100/min (the api tier) + * is too tight for legitimate agent workloads. Per-customer budgets are + * enforced separately by `McpRateLimiter` inside the handler against the + * `apiKey.rateLimit` field — this section limiter is the coarse ceiling. + * + * Wired into the middleware via `RATE_LIMIT_TIERS['mcp']` and the + * `/api/v1/mcp/` rule in `rate-limit-policy.ts`, keyed by api-key (not IP) + * so two customers sharing a NAT'd egress get independent buckets. Route + * handlers should NOT call `.check()` directly — the dispatcher already + * applied this cap. + */ +export const mcpLimiter = createRateLimiter({ + interval: SECURITY_CONSTANTS.RATE_LIMIT.DEFAULT_INTERVAL, + maxRequests: SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.MCP, + uniqueTokenPerInterval: SECURITY_CONSTANTS.RATE_LIMIT.MAX_UNIQUE_TOKENS, +}); + /** * Rate limiter for accept-invite endpoint * Limit: 5 attempts per 15 minutes per IP @@ -395,12 +417,13 @@ export const inboundLimiter = createRateLimiter({ * - `'admin'` — core admin endpoints (users, logs, invitations, feature flags, stats). 30/min. * - `'orchestration'` — admin/orchestration UI (agents, workflows, knowledge, executions). 120/min. * - `'api'` — general authenticated API + consumer surfaces. 100/min. + * - `'mcp'` — MCP transport endpoint (LLM-agent tool calls). 300/min per api-key. * - `'auth'` — authentication endpoints (login, signup, password reset). 5/min per IP. * * Add new tiers here, add a matching entry to {@link RATE_LIMIT_TIERS}, then * reference the tier from `RATE_LIMIT_POLICY`. */ -export type RateLimitTier = 'admin' | 'orchestration' | 'api' | 'auth'; +export type RateLimitTier = 'admin' | 'orchestration' | 'api' | 'mcp' | 'auth'; /** * Resolve a tier name to its concrete limiter instance. @@ -412,6 +435,7 @@ export const RATE_LIMIT_TIERS: Record = { admin: adminLimiter, orchestration: orchestrationAdminLimiter, api: apiLimiter, + mcp: mcpLimiter, auth: authLimiter, }; diff --git a/tests/unit/lib/security/constants.test.ts b/tests/unit/lib/security/constants.test.ts index 409796dda..5be1618c6 100644 --- a/tests/unit/lib/security/constants.test.ts +++ b/tests/unit/lib/security/constants.test.ts @@ -12,10 +12,12 @@ * a fresh module instance with whatever env state the test arranges. * * Tests use `RATE_LIMIT_API` as the probe for paths 2 & 3 (avoiding ADMIN / - * ORCH_ADMIN unless independence is the thing being verified). Default values: + * ORCH_ADMIN / MCP unless independence is the thing being verified). Default + * values: * RATE_LIMIT_API → 100 * RATE_LIMIT_ADMIN → 30 * RATE_LIMIT_ORCH_ADMIN → 120 + * RATE_LIMIT_MCP → 300 * * @see lib/security/constants.ts */ @@ -27,11 +29,13 @@ describe('SECURITY_CONSTANTS.RATE_LIMIT.LIMITS — envInt env-var overrides', () let savedApi: string | undefined; let savedAdmin: string | undefined; let savedOrchAdmin: string | undefined; + let savedMcp: string | undefined; beforeEach(() => { savedApi = process.env.RATE_LIMIT_API; savedAdmin = process.env.RATE_LIMIT_ADMIN; savedOrchAdmin = process.env.RATE_LIMIT_ORCH_ADMIN; + savedMcp = process.env.RATE_LIMIT_MCP; }); afterEach(() => { @@ -51,6 +55,11 @@ describe('SECURITY_CONSTANTS.RATE_LIMIT.LIMITS — envInt env-var overrides', () } else { process.env.RATE_LIMIT_ORCH_ADMIN = savedOrchAdmin; } + if (savedMcp === undefined) { + delete process.env.RATE_LIMIT_MCP; + } else { + process.env.RATE_LIMIT_MCP = savedMcp; + } // Always reset modules after each test so the next test's import is fresh. vi.resetModules(); @@ -129,13 +138,46 @@ describe('SECURITY_CONSTANTS.RATE_LIMIT.LIMITS — envInt env-var overrides', () expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.API).toBe(100); }); - // ── ADMIN and ORCH_ADMIN are overridden independently ──────────────────── + // ── MCP default ────────────────────────────────────────────────────────── + + it('uses the documented 300/min default for MCP when the env var is unset', async () => { + // Arrange: MCP defaults to 300 — a deliberate uplift over the api tier + // (100) because MCP is server-to-server agent traffic, not human-paced. + // This test pins the documented default so a stray edit to the constant + // is caught immediately. + delete process.env.RATE_LIMIT_MCP; + vi.resetModules(); + + // Act + const { SECURITY_CONSTANTS } = await import('@/lib/security/constants'); + + // Assert + expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.MCP).toBe(300); + }); + + it('applies the env override to MCP when set to a positive integer', async () => { + // Arrange + process.env.RATE_LIMIT_MCP = '1500'; + vi.resetModules(); + + // Act + const { SECURITY_CONSTANTS } = await import('@/lib/security/constants'); + + // Assert: env var routes through the shared envInt() helper, so a positive + // integer is parsed exactly the same as for ADMIN / API / ORCH_ADMIN. + expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.MCP).toBe(1500); + }); + + // ── ADMIN, ORCH_ADMIN, and MCP are overridden independently ────────────── - it('applies the override independently to ADMIN and ORCH_ADMIN limits', async () => { - // Arrange: set distinct values for each of the three overrideable limits + it('applies the override independently to API, ADMIN, ORCH_ADMIN, and MCP limits', async () => { + // Arrange: set distinct values for each of the four overrideable limits. + // This guards against a future refactor accidentally sharing state across + // the envInt() invocations. process.env.RATE_LIMIT_API = '200'; process.env.RATE_LIMIT_ADMIN = '60'; process.env.RATE_LIMIT_ORCH_ADMIN = '240'; + process.env.RATE_LIMIT_MCP = '600'; vi.resetModules(); // Act @@ -145,16 +187,18 @@ describe('SECURITY_CONSTANTS.RATE_LIMIT.LIMITS — envInt env-var overrides', () expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.API).toBe(200); expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.ADMIN).toBe(60); expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.ORCH_ADMIN).toBe(240); + expect(SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.MCP).toBe(600); }); // ── Constants not controlled by env vars remain unchanged ───────────────── it('leaves non-configurable constants (AUTH, PASSWORD_RESET, CONTACT) at their hardcoded values', async () => { - // Arrange: set all three overrideable vars to prove they don't bleed into + // Arrange: set all four overrideable vars to prove they don't bleed into // the hardcoded constants process.env.RATE_LIMIT_API = '999'; process.env.RATE_LIMIT_ADMIN = '999'; process.env.RATE_LIMIT_ORCH_ADMIN = '999'; + process.env.RATE_LIMIT_MCP = '999'; vi.resetModules(); // Act diff --git a/tests/unit/lib/security/rate-limit-policy.test.ts b/tests/unit/lib/security/rate-limit-policy.test.ts index 499cfd3a6..5edaaa07e 100644 --- a/tests/unit/lib/security/rate-limit-policy.test.ts +++ b/tests/unit/lib/security/rate-limit-policy.test.ts @@ -83,6 +83,41 @@ describe('rate-limit-policy', () => { expect(userRule?.key).toBe('session-user'); }); + it("MCP transport paths use the dedicated 'mcp' tier with 'api-key' keying", () => { + // Arrange — MCP is a distinct interface from the human-facing REST API: + // server-to-server, always API-key-authenticated, much chattier per + // session. It gets its own tier (300/min) keyed by API key so two + // customers sharing a NAT'd egress get independent buckets. This locks + // in that an MCP request does NOT fall through to the api catch-all, + // which would key on session-user and (since MCP has no session) fall + // back to IP — collapsing two customers behind the same egress. + const pathname = '/api/v1/mcp/anything'; + + // Act + const rule = findRateLimitRule(pathname); + + // Assert — tier MUST be 'mcp', not 'api'; key MUST be 'api-key', not 'session-user' + expect(rule).not.toBeNull(); + expect(rule?.tier).toBe('mcp'); + expect(rule?.key).toBe('api-key'); + }); + + it("the bare /api/v1/mcp path (no trailing slash) also resolves to the 'mcp' tier", () => { + // Arrange — the MCP rule uses `(\/|$)` boundary so the bare path matches. + // The Streamable HTTP transport accepts POST/GET/DELETE at /api/v1/mcp + // directly (no sub-path), and Next.js routes such requests to the same + // route handler. The rule MUST resolve regardless of trailing slash. + const pathname = '/api/v1/mcp'; + + // Act + const rule = findRateLimitRule(pathname); + + // Assert + expect(rule).not.toBeNull(); + expect(rule?.tier).toBe('mcp'); + expect(rule?.key).toBe('api-key'); + }); + it("webhook paths use the 'api' tier with 'api-key' keying", () => { // Arrange — webhook callers authenticate via Authorization: Bearer , // not session cookies. Keying on the API key (instead of session-user or IP) @@ -178,14 +213,17 @@ describe('rate-limit-policy', () => { expect(rule?.tier).not.toBe('admin'); }); - it('consumer-specific rules resolve before the api catch-all', () => { - // Arrange — each consumer rule (webhooks, embed, inbound, contact) needs to - // resolve to its own keying strategy, NOT fall through to the catch-all's - // 'session-user' key. If a consumer rule is accidentally moved below the - // catch-all, this test fails: the catch-all matches `/api/v1/anything` so - // it would silently swallow webhooks/embed/inbound/contact traffic with the - // wrong keying. That's a real security regression worth a dedicated guard. + it('MCP + consumer-specific rules resolve before the api catch-all', () => { + // Arrange — each non-session-keyed rule (mcp, webhooks, embed, inbound, + // contact) needs to resolve to its own keying strategy, NOT fall through + // to the catch-all's 'session-user' key. If any of these is accidentally + // moved below the catch-all, this test fails: the catch-all matches + // `/api/v1/anything` so it would silently swallow MCP / webhooks / embed / + // inbound / contact traffic with the wrong keying. That's a real security + // regression worth a dedicated guard. const cases: Array<[string, 'api-key' | 'embed-token' | 'ip']> = [ + ['/api/v1/mcp', 'api-key'], + ['/api/v1/mcp/whatever', 'api-key'], ['/api/v1/webhooks/trigger', 'api-key'], ['/api/v1/embed/chat', 'embed-token'], ['/api/v1/inbound/slack/agent-slug', 'ip'], @@ -219,19 +257,21 @@ describe('rate-limit-policy', () => { expect(RATE_LIMIT_POLICY[1].tier).toBe('admin'); expect(RATE_LIMIT_POLICY[2].tier).toBe('auth'); // /api/v1/auth/ expect(RATE_LIMIT_POLICY[3].tier).toBe('auth'); // /api/auth/ (better-auth routes) + // MCP transport — distinct interface, distinct tier, before the api consumer block. + expect(RATE_LIMIT_POLICY[4]).toMatchObject({ tier: 'mcp', key: 'api-key' }); // mcp // Consumer-specific rules — same tier ('api') but distinct keying. - expect(RATE_LIMIT_POLICY[4]).toMatchObject({ tier: 'api', key: 'api-key' }); // webhooks - expect(RATE_LIMIT_POLICY[5]).toMatchObject({ tier: 'api', key: 'embed-token' }); // embed - expect(RATE_LIMIT_POLICY[6]).toMatchObject({ tier: 'api', key: 'ip' }); // inbound - expect(RATE_LIMIT_POLICY[7]).toMatchObject({ tier: 'api', key: 'ip' }); // contact + expect(RATE_LIMIT_POLICY[5]).toMatchObject({ tier: 'api', key: 'api-key' }); // webhooks + expect(RATE_LIMIT_POLICY[6]).toMatchObject({ tier: 'api', key: 'embed-token' }); // embed + expect(RATE_LIMIT_POLICY[7]).toMatchObject({ tier: 'api', key: 'ip' }); // inbound + expect(RATE_LIMIT_POLICY[8]).toMatchObject({ tier: 'api', key: 'ip' }); // contact // Catch-all — must remain LAST so the consumer rules above it have a chance to match. - expect(RATE_LIMIT_POLICY[8]).toMatchObject({ tier: 'api', key: 'session-user' }); + expect(RATE_LIMIT_POLICY[9]).toMatchObject({ tier: 'api', key: 'session-user' }); }); - it('has exactly 9 rules (catches unintended additions or deletions)', () => { + it('has exactly 10 rules (catches unintended additions or deletions)', () => { // A length change is a signal that the policy changed. This test surfaces // that signal without being prescriptive about what was added/removed. - expect(RATE_LIMIT_POLICY).toHaveLength(9); + expect(RATE_LIMIT_POLICY).toHaveLength(10); }); }); diff --git a/tests/unit/lib/security/rate-limit.test.ts b/tests/unit/lib/security/rate-limit.test.ts index ad22699a0..b214a06ef 100644 --- a/tests/unit/lib/security/rate-limit.test.ts +++ b/tests/unit/lib/security/rate-limit.test.ts @@ -17,6 +17,7 @@ import { passwordResetLimiter, adminLimiter, orchestrationAdminLimiter, + mcpLimiter, RATE_LIMIT_TIERS, getRateLimitHeaders, createRateLimitResponse, @@ -582,6 +583,25 @@ describe('Rate Limiter', () => { } }); + it('mcpLimiter is configured with the mcp tier limit (default 300)', () => { + // Arrange: unique token to avoid cross-test bucket contamination + const token = `mcp-test-${Date.now()}`; + + try { + // Act: single check exercises the constant → config → instance integration path + const result = mcpLimiter.check(token); + + // Assert: result.limit is what the limiter was configured with — not a raw constant read. + // The 300/min default is higher than other tiers because MCP traffic is server-to-server + // agent calls inside conversations, which is much chattier than human-driven REST. + expect(result.limit).toBe(300); + // test-review:accept tobe_true — structural assertion verifying the limiter accepted the first request + expect(result.success).toBe(true); + } finally { + mcpLimiter.reset(token); + } + }); + it('RATE_LIMIT_TIERS resolves tier names to the correct singleton limiter instances', () => { // Arrange: no setup needed — registry is module-level state @@ -591,6 +611,7 @@ describe('Rate Limiter', () => { expect(RATE_LIMIT_TIERS.admin).toBe(adminLimiter); expect(RATE_LIMIT_TIERS.orchestration).toBe(orchestrationAdminLimiter); expect(RATE_LIMIT_TIERS.api).toBe(apiLimiter); + expect(RATE_LIMIT_TIERS.mcp).toBe(mcpLimiter); expect(RATE_LIMIT_TIERS.auth).toBe(authLimiter); }); @@ -615,6 +636,31 @@ describe('Rate Limiter', () => { RATE_LIMIT_TIERS.orchestration.reset(token); } }); + + it('RATE_LIMIT_TIERS.mcp enforces its configured limit when the bucket is exhausted', () => { + // Arrange: unique token so exhaustion in this test cannot bleed into neighbours. + // Mirrors the orchestration-tier enforcement test above; the value is asserting + // that the mcp tier entry in the registry is a working limiter, not just a config + // dictionary lookup. 300/min is the documented mcp cap. + const token = `tier-mcp-exhaust-${Date.now()}`; + + try { + // Act: exhaust the 300-request budget + for (let i = 0; i < 300; i++) { + const result = RATE_LIMIT_TIERS.mcp.check(token); + // test-review:accept tobe_true — structural assertion verifying each of 300 allowed requests succeeds + expect(result.success).toBe(true); + expect(result.remaining).toBe(299 - i); + } + + // Assert: 301st request must be denied — proves the registry entry actually rate-limits + const blocked = RATE_LIMIT_TIERS.mcp.check(token); + expect(blocked.success).toBe(false); + expect(blocked.remaining).toBe(0); + } finally { + RATE_LIMIT_TIERS.mcp.reset(token); + } + }); }); describe('Response helpers', () => { From cd76488fbc731d5e4d8fd6ca81e002f0683e8366 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Fri, 22 May 2026 23:18:57 +0100 Subject: [PATCH 2/6] refactor(security): drop redundant adminLimiter calls from admin handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section-level rate limiting for /api/v1/admin/** is now enforced centrally by proxy.ts via the policy table (the admin and orchestration tiers from PR #211). Per-handler adminLimiter.check calls became redundant defense- in-depth on PR #211's merge — this commit removes the now-dead code along with its tests. Source changes (139 handlers under app/api/v1/admin/orchestration/): - Remove `const rateLimit = adminLimiter.check()` + the `if (!rateLimit.success) return createRateLimitResponse(rateLimit)` check from every handler. - Strip the `const clientIP = getClientIP(request)` declaration when clientIP is no longer used in the handler body (per-handler scope analysis via brace-matching). Preserve it in 57 handlers that still use clientIP for `logAdminAction({ clientIp: clientIP })` audit logging. - Rename the handler `request` arg to `_request` when removing `getClientIP(request)` left it unused (TypeScript's noUnusedLocals). - Clean up imports: drop `adminLimiter`, `createRateLimitResponse`, and `getClientIP` from `@/lib/security/rate-limit` / `@/lib/security/ip` when they're no longer referenced (multi-line import blocks handled). - Update two stale docstrings: `costs/route.ts` and `settings/route.ts` both said "rate-limited via `adminLimiter`" — now point at the middleware policy table. Special-case preservation: - `conversations/export/route.ts` — KEPT its `adminLimiter.check(\`export:${ip}\`)` call. This is a deliberate per-flow custom-token sub-cap on the admin bucket, not a section-level redundancy. The codemod detected the template-literal token shape and skipped the file. - All per-flow sub-limiters (chatLimiter, audioLimiter, imageLimiter, contactLimiter, inboundLimiter, embedChatLimiter, etc.) untouched. Test changes (143 test files under tests/{unit,integration}/.../admin/): - Delete `describe('...Rate [lL]imit{ing}...', ...)` blocks when the block body references adminLimiter (false-positive guard: blocks for other limiters like cspReportLimiter survive). - Delete standalone `it('returns 429 when ...')` / `it('calls adminLimiter.check ...')` blocks whose body references adminLimiter. - Delete `vi.mock('@/lib/security/rate-limit', ...)` blocks that ONLY mocked adminLimiter and createRateLimitResponse — preserve mocks that also carry sub-limiters. - Delete stray `vi.mocked(adminLimiter.check).mockReturnValue(...)` setup lines (with multi-line `();` handling). - Conditionally delete `vi.mocked(createRateLimitResponse).mockReturnValue(...)` setups: only when the file's vi.mock for `@/lib/security/rate-limit` was also deleted (otherwise the symbol is still mocked and the setup is legitimate — preserves cspReportLimiter test in csp-report/route.test.ts). - Clean up dead imports of adminLimiter / createRateLimitResponse from test files (multi-line aware), excluding mock-factory bodies and comment references from the "still used" count. ESLint config: add `.claude/tmp/**` to the ignore list so the throwaway codemod scripts (used to drive this sweep) don't trigger lint errors during `npm run validate` — same rationale as the existing `.claude/worktrees/**` exclusion. Net diff: 283 files changed, +25 / -4,373 lines 139 source files, 143 test files, 1 config file Validation: Type-check: clean Lint: clean (524 pre-existing warnings, 0 new errors) Format: clean Tests: 850/850 files passing, 17,734 tests (-173 deleted dead tests), 6 skipped, 10 todo The 173 deleted tests covered the contract "handler calls adminLimiter.check; on failure returns 429" — that contract has moved to the middleware dispatcher and is covered by tests at the right layer in tests/unit/lib/security/rate-limit-middleware.test.ts (the limiter-exhaustion → 429 + envelope shape suite added in PR #211). Co-Authored-By: Claude Opus 4.7 --- .../agent-profiles/[id]/route.ts | 5 -- .../orchestration/agent-profiles/route.ts | 3 - .../orchestration/agents/[id]/budget/route.ts | 6 -- .../agents/[id]/capabilities/[capId]/route.ts | 5 -- .../agents/[id]/capabilities/route.ts | 7 -- .../agents/[id]/capabilities/usage/route.ts | 6 -- .../orchestration/agents/[id]/clone/route.ts | 3 - .../[id]/embed-tokens/[tokenId]/route.ts | 5 -- .../agents/[id]/embed-tokens/route.ts | 7 -- .../agents/[id]/instructions-history/route.ts | 6 -- .../agents/[id]/instructions-revert/route.ts | 3 - .../[id]/invite-tokens/[tokenId]/route.ts | 3 - .../agents/[id]/invite-tokens/route.ts | 9 +- .../admin/orchestration/agents/[id]/route.ts | 9 -- .../versions/[versionId]/restore/route.ts | 3 - .../agents/[id]/versions/[versionId]/route.ts | 8 +- .../agents/[id]/versions/route.ts | 6 -- .../agents/[id]/widget-config/route.ts | 9 +- .../admin/orchestration/agents/bulk/route.ts | 3 - .../orchestration/agents/compare/route.ts | 6 -- .../orchestration/agents/export/route.ts | 6 -- .../orchestration/agents/import/route.ts | 3 - .../v1/admin/orchestration/agents/route.ts | 7 -- .../analytics/content-gaps/route.ts | 6 -- .../analytics/engagement/route.ts | 6 -- .../orchestration/analytics/feedback/route.ts | 6 -- .../orchestration/analytics/topics/route.ts | 6 -- .../analytics/unanswered/route.ts | 6 -- .../orchestration/approvals/history/route.ts | 6 -- .../v1/admin/orchestration/audit-log/route.ts | 6 -- .../orchestration/backup/export/route.ts | 3 - .../orchestration/backup/import/route.ts | 3 - .../capabilities/[id]/agents/route.ts | 6 -- .../orchestration/capabilities/[id]/route.ts | 5 -- .../admin/orchestration/capabilities/route.ts | 3 - .../admin/orchestration/chat/stream/route.ts | 6 -- .../conversations/[id]/provenance.md/route.ts | 5 -- .../conversations/[id]/provenance/route.ts | 5 -- .../orchestration/conversations/[id]/route.ts | 9 -- .../conversations/clear/route.ts | 3 - .../admin/orchestration/costs/alerts/route.ts | 6 -- app/api/v1/admin/orchestration/costs/route.ts | 10 +-- .../orchestration/costs/summary/route.ts | 6 -- .../orchestration/discovery/models/route.ts | 6 -- .../orchestration/embedding-models/route.ts | 6 -- .../evaluations/[id]/complete/route.ts | 6 -- .../evaluations/[id]/rescore/route.ts | 6 -- .../orchestration/evaluations/[id]/route.ts | 6 -- .../admin/orchestration/evaluations/route.ts | 6 -- .../executions/[id]/approve/route.ts | 6 -- .../executions/[id]/cancel/route.ts | 6 -- .../executions/[id]/force-fail/route.ts | 3 - .../executions/[id]/lease/route.ts | 6 -- .../executions/[id]/live/route.ts | 8 +- .../executions/[id]/reject/route.ts | 6 -- .../executions/[id]/report.md/route.ts | 8 +- .../executions/[id]/rerun/route.ts | 6 -- .../executions/[id]/retry-step/route.ts | 6 -- .../executions/[id]/review/route.ts | 6 -- .../orchestration/executions/[id]/route.ts | 8 +- .../executions/[id]/status/route.ts | 8 +- .../orchestration/executions/counts/route.ts | 6 -- .../orchestration/executions/live/route.ts | 6 -- .../admin/orchestration/executions/route.ts | 6 -- .../orchestration/experiments/[id]/route.ts | 5 -- .../experiments/[id]/run/route.ts | 3 - .../admin/orchestration/experiments/route.ts | 7 -- .../hooks/[id]/deliveries/route.ts | 6 -- .../hooks/[id]/rotate-secret/route.ts | 5 -- .../admin/orchestration/hooks/[id]/route.ts | 13 --- .../hooks/deliveries/[id]/retry/route.ts | 3 - app/api/v1/admin/orchestration/hooks/route.ts | 7 -- .../knowledge/documents/[id]/chunks/route.ts | 6 -- .../knowledge/documents/[id]/confirm/route.ts | 3 - .../documents/[id]/enrich-keywords/route.ts | 3 - .../knowledge/documents/[id]/rechunk/route.ts | 3 - .../knowledge/documents/[id]/retry/route.ts | 3 - .../knowledge/documents/[id]/route.ts | 5 -- .../knowledge/documents/bulk/route.ts | 3 - .../knowledge/documents/fetch-url/route.ts | 3 - .../knowledge/documents/route.ts | 3 - .../orchestration/knowledge/embed/route.ts | 3 - .../knowledge/embedding-status/route.ts | 8 +- .../knowledge/embeddings/route.ts | 6 -- .../orchestration/knowledge/graph/route.ts | 6 -- .../knowledge/patterns/[number]/route.ts | 6 -- .../orchestration/knowledge/patterns/route.ts | 6 -- .../orchestration/knowledge/search/route.ts | 6 -- .../orchestration/knowledge/seed/route.ts | 3 - .../knowledge/tags/[id]/route.ts | 5 -- .../orchestration/knowledge/tags/route.ts | 3 - .../orchestration/maintenance/tick/route.ts | 8 +- .../v1/admin/orchestration/mcp/audit/route.ts | 10 --- .../mcp/keys/[id]/rotate/route.ts | 3 - .../orchestration/mcp/keys/[id]/route.ts | 5 -- .../v1/admin/orchestration/mcp/keys/route.ts | 7 -- .../orchestration/mcp/resources/[id]/route.ts | 10 --- .../orchestration/mcp/resources/route.ts | 10 --- .../orchestration/mcp/sessions/[id]/route.ts | 6 -- .../admin/orchestration/mcp/sessions/route.ts | 6 -- .../admin/orchestration/mcp/settings/route.ts | 7 -- .../orchestration/mcp/tools/[id]/route.ts | 10 --- .../v1/admin/orchestration/mcp/tools/route.ts | 10 --- .../v1/admin/orchestration/models/route.ts | 5 -- .../observability/dashboard-stats/route.ts | 6 -- .../provider-models/[id]/route.ts | 10 --- .../provider-models/bulk/route.ts | 6 -- .../orchestration/provider-models/route.ts | 6 +- .../providers/[id]/health/route.ts | 3 - .../providers/[id]/models/route.ts | 6 -- .../orchestration/providers/[id]/route.ts | 5 -- .../providers/[id]/test-model/route.ts | 6 -- .../providers/[id]/test/route.ts | 6 -- .../v1/admin/orchestration/providers/route.ts | 3 - .../providers/test-bulk/route.ts | 6 -- .../admin/orchestration/quiz-scores/route.ts | 10 --- .../orchestration/schedules/tick/route.ts | 8 +- .../v1/admin/orchestration/settings/route.ts | 14 +-- .../webhooks/[id]/deliveries/route.ts | 6 -- .../orchestration/webhooks/[id]/route.ts | 15 +--- .../orchestration/webhooks/[id]/test/route.ts | 6 -- .../webhooks/deliveries/[id]/retry/route.ts | 3 - .../v1/admin/orchestration/webhooks/route.ts | 7 -- .../workflows/[id]/cost-estimate/route.ts | 10 --- .../workflows/[id]/discard-draft/route.ts | 3 - .../workflows/[id]/dry-run/route.ts | 6 -- .../workflows/[id]/execute-stream/route.ts | 6 -- .../workflows/[id]/execute/route.ts | 6 -- .../workflows/[id]/publish/route.ts | 3 - .../workflows/[id]/rollback/route.ts | 3 - .../orchestration/workflows/[id]/route.ts | 9 -- .../workflows/[id]/save-as-template/route.ts | 3 - .../[id]/schedules/[scheduleId]/route.ts | 11 +-- .../workflows/[id]/schedules/route.ts | 9 +- .../workflows/[id]/validate/route.ts | 6 -- .../[id]/versions/[version]/route.ts | 6 -- .../workflows/[id]/versions/route.ts | 6 -- .../v1/admin/orchestration/workflows/route.ts | 7 -- .../workflows/templates/route.ts | 5 -- eslint.config.mjs | 3 + .../orchestration/agent-profiles.id.test.ts | 7 -- .../orchestration/agent-profiles.test.ts | 16 ---- .../agent-version-restore.test.ts | 7 -- .../agents.export-import-roundtrip.test.ts | 7 -- .../admin/orchestration/agents.export.test.ts | 32 ------- .../agents.id.capabilities.test.ts | 56 ------------ .../agents.id.capabilities.usage.test.ts | 19 ---- .../orchestration/agents.id.clone.test.ts | 20 ----- .../agents.id.instructions-revert.test.ts | 21 ----- .../v1/admin/orchestration/agents.id.test.ts | 46 ---------- .../admin/orchestration/agents.import.test.ts | 19 ---- .../agents.invite-tokens.test.ts | 54 ------------ .../api/v1/admin/orchestration/agents.test.ts | 29 ------- .../orchestration/approvals.history.test.ts | 19 ---- .../admin/orchestration/backup/export.test.ts | 21 ----- .../admin/orchestration/backup/import.test.ts | 20 ----- .../capabilities.id.agents.test.ts | 20 ----- .../admin/orchestration/capabilities.test.ts | 49 ----------- .../admin/orchestration/chat.stream.test.ts | 70 --------------- .../orchestration/conversations.clear.test.ts | 20 ----- .../conversations.id.get.test.ts | 7 -- .../conversations.id.patch.test.ts | 7 -- .../orchestration/conversations.id.test.ts | 21 ----- .../orchestration/discovery.models.test.ts | 18 ---- .../orchestration/embedding-models.test.ts | 34 -------- .../evaluations.id.complete.test.ts | 20 ----- .../evaluations.id.rescore.test.ts | 9 -- .../orchestration/evaluations.id.test.ts | 21 ----- .../admin/orchestration/evaluations.test.ts | 21 ----- .../executions.id.approve.test.ts | 16 ---- .../executions.id.cancel.test.ts | 18 ---- .../orchestration/executions.id.live.test.ts | 21 ----- .../executions.id.reject.test.ts | 16 ---- .../executions.id.status.test.ts | 22 ----- .../orchestration/executions.list.test.ts | 7 -- .../orchestration/experiments.id.run.test.ts | 18 ---- .../orchestration/experiments.id.test.ts | 28 ------ .../admin/orchestration/experiments.test.ts | 20 ----- .../knowledge-document-tags.test.ts | 7 -- .../knowledge-enrich-keywords.test.ts | 27 ------ .../orchestration/knowledge-tags.test.ts | 7 -- .../knowledge.documents.bulk.test.ts | 23 ----- .../knowledge.documents.fetch-url.test.ts | 20 ----- .../knowledge.documents.id.chunks.test.ts | 24 ------ .../knowledge.documents.id.confirm.test.ts | 24 ------ .../knowledge.documents.id.rechunk.test.ts | 23 ----- .../knowledge.documents.id.retry.test.ts | 20 ----- .../knowledge.documents.id.test.ts | 21 ----- .../orchestration/knowledge.documents.test.ts | 44 ---------- .../orchestration/knowledge.embed.test.ts | 21 ----- .../knowledge.embedding-status.test.ts | 20 ----- .../orchestration/knowledge.graph.test.ts | 19 ---- .../knowledge.patterns.number.test.ts | 7 -- .../orchestration/knowledge.patterns.test.ts | 21 ----- .../orchestration/knowledge.search.test.ts | 30 ------- .../orchestration/knowledge.seed.test.ts | 30 ------- .../orchestration/knowledge.tags.id.test.ts | 57 ------------ .../orchestration/maintenance.tick.test.ts | 7 -- .../api/v1/admin/orchestration/models.test.ts | 37 -------- .../observability.dashboard-stats.test.ts | 7 -- .../provider-models.bulk.test.ts | 18 ---- .../orchestration/provider-models.id.test.ts | 29 ------- .../orchestration/provider-models.test.ts | 11 +-- .../orchestration/providers.id.health.test.ts | 30 ------- .../orchestration/providers.id.models.test.ts | 18 ---- .../providers.id.test-connection.test.ts | 20 ----- .../providers.id.test-model.test.ts | 20 ----- .../admin/orchestration/providers.id.test.ts | 50 ----------- .../orchestration/providers.test-bulk.test.ts | 23 ----- .../v1/admin/orchestration/providers.test.ts | 20 ----- .../admin/orchestration/quiz-scores.test.ts | 20 ----- .../v1/admin/orchestration/settings.test.ts | 51 ----------- .../admin/orchestration/webhooks.id.test.ts | 33 ------- .../webhooks/[id]/test/route.test.ts | 44 ---------- .../workflows.id.cost-estimate.test.ts | 30 ------- .../workflows.id.dry-run.test.ts | 20 ----- .../workflows.id.execute.test.ts | 20 ----- .../admin/orchestration/workflows.id.test.ts | 49 ----------- .../workflows.id.validate.test.ts | 20 ----- .../v1/admin/orchestration/workflows.test.ts | 20 ----- .../workflows.versioning.test.ts | 86 ------------------- .../[versionId]/restore/route.test.ts | 30 ------- .../agent-system-protection.route.test.ts | 7 -- .../agents/capabilities-usage.route.test.ts | 23 ----- .../embed-tokens/[tokenId]/route.test.ts | 5 -- .../agents/embed-tokens/route.test.ts | 5 -- .../invite-tokens/invite-tokens.route.test.ts | 50 ----------- .../agents/versions/route.test.ts | 9 -- .../agents/widget-config/route.test.ts | 25 ------ .../analytics/analytics.route.test.ts | 74 ---------------- .../orchestration/audit-log/route.test.ts | 40 --------- ...capability-system-protection.route.test.ts | 7 -- .../orchestration/chat/stream/route.test.ts | 13 --- .../conversations/export/route.test.ts | 16 ---- .../conversations/search/route.test.ts | 5 -- .../orchestration/costs/alerts/route.test.ts | 20 ----- .../admin/orchestration/costs/route.test.ts | 20 ----- .../orchestration/costs/summary/route.test.ts | 28 ------ .../executions/[id]/force-fail/route.test.ts | 24 ------ .../executions/[id]/lease/route.test.ts | 13 --- .../executions/counts/route.test.ts | 32 ------- .../executions/live/route.test.ts | 31 ------- .../executions/report-md/route.test.ts | 13 --- .../executions/rerun/route.test.ts | 6 -- .../executions/review/route.test.ts | 6 -- .../orchestration/executions/route.test.ts | 37 -------- .../hooks/[id]/rotate-secret/route.test.ts | 37 -------- .../orchestration/hooks/[id]/route.test.ts | 43 ---------- .../hooks/deliveries/[id]/retry/route.test.ts | 18 ---- .../admin/orchestration/hooks/route.test.ts | 66 -------------- .../documents/[id]/confirm/route.test.ts | 35 -------- .../knowledge/documents/route.test.ts | 42 --------- .../knowledge/embedding-status/route.test.ts | 7 -- .../knowledge/embeddings/route.test.ts | 15 ---- .../knowledge/graph/route.test.ts | 22 ----- .../knowledge/patterns/route.test.ts | 7 -- .../maintenance/tick/route.test.ts | 39 --------- .../orchestration/mcp/audit/route.test.ts | 31 ------- .../mcp/keys/[id]/rotate/route.test.ts | 20 ----- .../orchestration/mcp/keys/[id]/route.test.ts | 31 ------- .../orchestration/mcp/keys/route.test.ts | 31 ------- .../mcp/resources/[id]/route.test.ts | 31 ------- .../orchestration/mcp/resources/route.test.ts | 31 ------- .../mcp/sessions/[id]/route.test.ts | 20 ----- .../orchestration/mcp/sessions/route.test.ts | 20 ----- .../orchestration/mcp/settings/route.test.ts | 31 ------- .../mcp/tools/[id]/route.test.ts | 31 ------- .../orchestration/mcp/tools/route.test.ts | 31 ------- .../dashboard-stats/route.test.ts | 35 -------- .../providers/test-model.route.test.ts | 35 -------- .../orchestration/quiz-scores/route.test.ts | 7 -- .../schedules/schedules.route.test.ts | 24 ------ .../orchestration/webhooks/[id]/route.test.ts | 43 ---------- .../webhooks/[id]/test/route.test.ts | 37 -------- .../webhooks/deliveries/retry/route.test.ts | 5 -- .../webhooks/deliveries/route.test.ts | 5 -- .../orchestration/webhooks/route.test.ts | 26 ------ .../orchestration/webhooks/test/route.test.ts | 24 ------ .../workflows/[id]/execute/route.test.ts | 30 ------- .../[id]/save-as-template/route.test.ts | 38 -------- .../[id]/schedules/[scheduleId]/route.test.ts | 60 ------------- .../workflows/execute-stream/route.test.ts | 7 -- .../workflows/templates/route.test.ts | 5 -- 283 files changed, 25 insertions(+), 4373 deletions(-) diff --git a/app/api/v1/admin/orchestration/agent-profiles/[id]/route.ts b/app/api/v1/admin/orchestration/agent-profiles/[id]/route.ts index e1f873226..d9766e290 100644 --- a/app/api/v1/admin/orchestration/agent-profiles/[id]/route.ts +++ b/app/api/v1/admin/orchestration/agent-profiles/[id]/route.ts @@ -22,7 +22,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { updateAgentProfileSchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; @@ -58,8 +57,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -105,8 +102,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/agent-profiles/route.ts b/app/api/v1/admin/orchestration/agent-profiles/route.ts index 69c6f75f0..a95276ec0 100644 --- a/app/api/v1/admin/orchestration/agent-profiles/route.ts +++ b/app/api/v1/admin/orchestration/agent-profiles/route.ts @@ -22,7 +22,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { agentProfileFormSchema, @@ -68,8 +67,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, agentProfileFormSchema); diff --git a/app/api/v1/admin/orchestration/agents/[id]/budget/route.ts b/app/api/v1/admin/orchestration/agents/[id]/budget/route.ts index 734531e4f..8b927bea1 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/budget/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/budget/route.ts @@ -19,16 +19,10 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { checkBudget } from '@/lib/orchestration/llm/cost-tracker'; export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/capabilities/[capId]/route.ts b/app/api/v1/admin/orchestration/agents/[id]/capabilities/[capId]/route.ts index 95c8d53b8..d8cc1f649 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/capabilities/[capId]/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/capabilities/[capId]/route.ts @@ -21,7 +21,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; import { findUnsetEnvVarReferences } from '@/lib/orchestration/env-template'; @@ -68,8 +67,6 @@ function parseIds(raw: RouteParams): { agentId: string; capabilityId: string } { export const PATCH = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { agentId, capabilityId } = parseIds(await params); @@ -120,8 +117,6 @@ export const PATCH = withAdminAuth(async (request, session, { param export const DELETE = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { agentId, capabilityId } = parseIds(await params); diff --git a/app/api/v1/admin/orchestration/agents/[id]/capabilities/route.ts b/app/api/v1/admin/orchestration/agents/[id]/capabilities/route.ts index 98b12eb7f..f155cd624 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/capabilities/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/capabilities/route.ts @@ -23,7 +23,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; import { findUnsetEnvVarReferences } from '@/lib/orchestration/env-template'; @@ -68,10 +67,6 @@ function parseAgentId(raw: string): string { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawAgentId } = await params; const agentId = parseAgentId(rawAgentId); @@ -91,8 +86,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawAgentId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/capabilities/usage/route.ts b/app/api/v1/admin/orchestration/agents/[id]/capabilities/usage/route.ts index a75c49722..c6ac0b533 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/capabilities/usage/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/capabilities/usage/route.ts @@ -15,8 +15,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; interface UsageRow { @@ -25,10 +23,6 @@ interface UsageRow { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/clone/route.ts b/app/api/v1/admin/orchestration/agents/[id]/clone/route.ts index 7e79218e8..bb3769400 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/clone/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/clone/route.ts @@ -18,7 +18,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError, ConflictError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cloneAgentBodySchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; @@ -26,8 +25,6 @@ import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/[tokenId]/route.ts b/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/[tokenId]/route.ts index ea8d0f723..228f48363 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/[tokenId]/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/[tokenId]/route.ts @@ -12,7 +12,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -29,8 +28,6 @@ async function findToken(agentId: string, tokenId: string) { export const PATCH = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawAgentId, tokenId: rawTokenId } = await params; const agentIdParsed = cuidSchema.safeParse(rawAgentId); @@ -73,8 +70,6 @@ export const PATCH = withAdminAuth(async (request, session, { params }) export const DELETE = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawAgentId, tokenId: rawTokenId } = await params; const agentIdParsed = cuidSchema.safeParse(rawAgentId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/route.ts b/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/route.ts index f0f51abfb..638ef74fc 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/embed-tokens/route.ts @@ -12,7 +12,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -22,10 +21,6 @@ import { cuidSchema } from '@/lib/validations/common'; type Params = { id: string }; export const GET = withAdminAuth(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) @@ -51,8 +46,6 @@ export const GET = withAdminAuth(async (request, _session, { params }) = export const POST = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/instructions-history/route.ts b/app/api/v1/admin/orchestration/agents/[id]/instructions-history/route.ts index 7117cf7f0..ec9049e50 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/instructions-history/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/instructions-history/route.ts @@ -19,8 +19,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { logger } from '@/lib/logging'; import { systemInstructionsHistorySchema, @@ -37,10 +35,6 @@ function parseAgentId(raw: string): string { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseAgentId(rawId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/instructions-revert/route.ts b/app/api/v1/admin/orchestration/agents/[id]/instructions-revert/route.ts index b9a912100..c2e7fd264 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/instructions-revert/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/instructions-revert/route.ts @@ -27,7 +27,6 @@ import { successResponse } from '@/lib/api/responses'; import { ForbiddenError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logger } from '@/lib/logging'; import { @@ -48,8 +47,6 @@ function parseAgentId(raw: string): string { export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/[tokenId]/route.ts b/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/[tokenId]/route.ts index 613062613..4bdf3e9ce 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/[tokenId]/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/[tokenId]/route.ts @@ -11,7 +11,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -20,8 +19,6 @@ type Params = { id: string; tokenId: string }; export const DELETE = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawAgentId, tokenId: rawTokenId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/route.ts b/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/route.ts index 4122b0572..a3716ecd4 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/route.ts @@ -13,7 +13,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { createInviteTokenSchema } from '@/lib/validations/orchestration'; @@ -21,11 +20,7 @@ import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; type Params = { id: string }; -export const GET = withAdminAuth(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth(async (_request, _session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) @@ -58,8 +53,6 @@ export const GET = withAdminAuth(async (request, _session, { params }) = export const POST = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/route.ts b/app/api/v1/admin/orchestration/agents/[id]/route.ts index d3508b985..b9899907f 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/route.ts @@ -20,7 +20,6 @@ import { successResponse } from '@/lib/api/responses'; import { ForbiddenError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction, computeChanges } from '@/lib/orchestration/audit/admin-audit-logger'; import { emitHookEvent } from '@/lib/orchestration/hooks/registry'; @@ -43,10 +42,6 @@ function parseAgentId(raw: string): string { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseAgentId(rawId); @@ -76,8 +71,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -433,8 +426,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.ts b/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.ts index ffb3d725f..53d07dd98 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.ts @@ -16,7 +16,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ForbiddenError, NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { getRouteLogger } from '@/lib/api/context'; import { cuidSchema } from '@/lib/validations/common'; @@ -65,8 +64,6 @@ const versionSnapshotSchema = z.object({ export const POST = withAdminAuth<{ id: string; versionId: string }>( async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId, versionId: rawVersionId } = await params; diff --git a/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/route.ts b/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/route.ts index 116274ad9..3136afd57 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/route.ts @@ -14,8 +14,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; function validateIds(rawAgentId: string, rawVersionId: string) { @@ -31,11 +29,7 @@ function validateIds(rawAgentId: string, rawVersionId: string) { } export const GET = withAdminAuth<{ id: string; versionId: string }>( - async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - + async (_request, _session, { params }) => { const { id: rawId, versionId: rawVersionId } = await params; const { agentId, versionId } = validateIds(rawId, rawVersionId); diff --git a/app/api/v1/admin/orchestration/agents/[id]/versions/route.ts b/app/api/v1/admin/orchestration/agents/[id]/versions/route.ts index 6159f6e40..6502ce4ff 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/versions/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/versions/route.ts @@ -13,15 +13,9 @@ import { prisma } from '@/lib/db/client'; import { paginatedResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema, paginationQuerySchema } from '@/lib/validations/common'; export const GET = withAdminAuth<{ id: string }>(async (_request, _session, { params }) => { - const clientIP = getClientIP(_request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) diff --git a/app/api/v1/admin/orchestration/agents/[id]/widget-config/route.ts b/app/api/v1/admin/orchestration/agents/[id]/widget-config/route.ts index 347cf42ef..6cc34c52b 100644 --- a/app/api/v1/admin/orchestration/agents/[id]/widget-config/route.ts +++ b/app/api/v1/admin/orchestration/agents/[id]/widget-config/route.ts @@ -12,7 +12,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -21,11 +20,7 @@ import { cuidSchema } from '@/lib/validations/common'; type Params = { id: string }; -export const GET = withAdminAuth(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth(async (_request, _session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) @@ -44,8 +39,6 @@ export const GET = withAdminAuth(async (request, _session, { params }) = export const PATCH = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/agents/bulk/route.ts b/app/api/v1/admin/orchestration/agents/bulk/route.ts index 4081231b6..b2fa1b605 100644 --- a/app/api/v1/admin/orchestration/agents/bulk/route.ts +++ b/app/api/v1/admin/orchestration/agents/bulk/route.ts @@ -17,15 +17,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { bulkAgentActionSchema } from '@/lib/validations/orchestration'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { action, agentIds } = await validateRequestBody(request, bulkAgentActionSchema); diff --git a/app/api/v1/admin/orchestration/agents/compare/route.ts b/app/api/v1/admin/orchestration/agents/compare/route.ts index d0f7d4136..62f057d6d 100644 --- a/app/api/v1/admin/orchestration/agents/compare/route.ts +++ b/app/api/v1/admin/orchestration/agents/compare/route.ts @@ -16,8 +16,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { cuidSchema } from '@/lib/validations/common'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; const querySchema = z.object({ agentIds: z @@ -70,10 +68,6 @@ async function getAgentStats(agentId: string) { } export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const url = new URL(request.url); const parsed = querySchema.safeParse({ agentIds: url.searchParams.get('agentIds') ?? '' }); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/agents/export/route.ts b/app/api/v1/admin/orchestration/agents/export/route.ts index 31330ba7a..01b92f8d5 100644 --- a/app/api/v1/admin/orchestration/agents/export/route.ts +++ b/app/api/v1/admin/orchestration/agents/export/route.ts @@ -21,8 +21,6 @@ import { NotFoundError } from '@/lib/api/errors'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { logger } from '@/lib/logging'; import { exportAgentsSchema, @@ -37,10 +35,6 @@ const visibilitySchema = z.enum(['internal', 'public', 'invite_only']); const reasoningEffortSchema = z.enum(['minimal', 'low', 'medium', 'high']).nullable().optional(); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, exportAgentsSchema); const uniqueIds = [...new Set(body.agentIds)]; diff --git a/app/api/v1/admin/orchestration/agents/import/route.ts b/app/api/v1/admin/orchestration/agents/import/route.ts index 1505e9ae5..7c02632a3 100644 --- a/app/api/v1/admin/orchestration/agents/import/route.ts +++ b/app/api/v1/admin/orchestration/agents/import/route.ts @@ -28,7 +28,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; import { importAgentsSchema } from '@/lib/validations/orchestration'; @@ -44,8 +43,6 @@ type ImportResults = { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, importAgentsSchema); diff --git a/app/api/v1/admin/orchestration/agents/route.ts b/app/api/v1/admin/orchestration/agents/route.ts index dd93d2ed3..e09da3698 100644 --- a/app/api/v1/admin/orchestration/agents/route.ts +++ b/app/api/v1/admin/orchestration/agents/route.ts @@ -14,7 +14,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { createAgentSchema, listAgentsQuerySchema } from '@/lib/validations/orchestration'; import { getMonthToDateGlobalSpend } from '@/lib/orchestration/llm/cost-tracker'; @@ -23,10 +22,6 @@ import { logger } from '@/lib/logging'; import type { BudgetSummary } from '@/types/orchestration'; export const GET = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); @@ -124,8 +119,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createAgentSchema); diff --git a/app/api/v1/admin/orchestration/analytics/content-gaps/route.ts b/app/api/v1/admin/orchestration/analytics/content-gaps/route.ts index 94115c949..34213a125 100644 --- a/app/api/v1/admin/orchestration/analytics/content-gaps/route.ts +++ b/app/api/v1/admin/orchestration/analytics/content-gaps/route.ts @@ -11,16 +11,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { analyticsQuerySchema } from '@/lib/validations/orchestration'; import { getContentGaps } from '@/lib/orchestration/analytics'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, analyticsQuerySchema); diff --git a/app/api/v1/admin/orchestration/analytics/engagement/route.ts b/app/api/v1/admin/orchestration/analytics/engagement/route.ts index 9f06de019..7ef4d4912 100644 --- a/app/api/v1/admin/orchestration/analytics/engagement/route.ts +++ b/app/api/v1/admin/orchestration/analytics/engagement/route.ts @@ -11,16 +11,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { analyticsQuerySchema } from '@/lib/validations/orchestration'; import { getEngagementMetrics } from '@/lib/orchestration/analytics'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, analyticsQuerySchema); diff --git a/app/api/v1/admin/orchestration/analytics/feedback/route.ts b/app/api/v1/admin/orchestration/analytics/feedback/route.ts index 54487796d..121476422 100644 --- a/app/api/v1/admin/orchestration/analytics/feedback/route.ts +++ b/app/api/v1/admin/orchestration/analytics/feedback/route.ts @@ -10,16 +10,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { analyticsQuerySchema } from '@/lib/validations/orchestration'; import { getFeedbackSummary } from '@/lib/orchestration/analytics'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, analyticsQuerySchema); diff --git a/app/api/v1/admin/orchestration/analytics/topics/route.ts b/app/api/v1/admin/orchestration/analytics/topics/route.ts index fdf46aed8..2a226f117 100644 --- a/app/api/v1/admin/orchestration/analytics/topics/route.ts +++ b/app/api/v1/admin/orchestration/analytics/topics/route.ts @@ -10,16 +10,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { analyticsQuerySchema } from '@/lib/validations/orchestration'; import { getPopularTopics } from '@/lib/orchestration/analytics'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, analyticsQuerySchema); diff --git a/app/api/v1/admin/orchestration/analytics/unanswered/route.ts b/app/api/v1/admin/orchestration/analytics/unanswered/route.ts index 7cb4f1a42..34a9383ba 100644 --- a/app/api/v1/admin/orchestration/analytics/unanswered/route.ts +++ b/app/api/v1/admin/orchestration/analytics/unanswered/route.ts @@ -11,16 +11,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { analyticsQuerySchema } from '@/lib/validations/orchestration'; import { getUnansweredQuestions } from '@/lib/orchestration/analytics'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, analyticsQuerySchema); diff --git a/app/api/v1/admin/orchestration/approvals/history/route.ts b/app/api/v1/admin/orchestration/approvals/history/route.ts index 96f742502..0fcc8e635 100644 --- a/app/api/v1/admin/orchestration/approvals/history/route.ts +++ b/app/api/v1/admin/orchestration/approvals/history/route.ts @@ -26,8 +26,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { csvEscape } from '@/lib/api/csv'; import { paginatedResponse } from '@/lib/api/responses'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getRouteLogger } from '@/lib/api/context'; import { approvalHistoryQuerySchema } from '@/lib/validations/orchestration'; import { executionTraceSchema } from '@/lib/validations/orchestration'; @@ -43,10 +41,6 @@ const MAX_EXECUTIONS_SCANNED = 1000; const MAX_CSV_ROWS = 5000; export const GET = withAdminAuth(async (request, session) => { - const ip = getClientIP(request); - const rl = adminLimiter.check(ip); - if (!rl.success) return createRateLimitResponse(rl); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); diff --git a/app/api/v1/admin/orchestration/audit-log/route.ts b/app/api/v1/admin/orchestration/audit-log/route.ts index 6c3d31628..ad65170a5 100644 --- a/app/api/v1/admin/orchestration/audit-log/route.ts +++ b/app/api/v1/admin/orchestration/audit-log/route.ts @@ -11,15 +11,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { paginatedResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { listAuditLogQuerySchema } from '@/lib/validations/orchestration'; export const GET = withAdminAuth(async (request, _session) => { - const clientIp = getClientIP(request); - const rateLimit = adminLimiter.check(clientIp); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const { page, limit, action, entityType, entityId, userId, dateFrom, dateTo, q } = validateQueryParams(searchParams, listAuditLogQuerySchema); diff --git a/app/api/v1/admin/orchestration/backup/export/route.ts b/app/api/v1/admin/orchestration/backup/export/route.ts index ff9e9e48b..5c891f8dd 100644 --- a/app/api/v1/admin/orchestration/backup/export/route.ts +++ b/app/api/v1/admin/orchestration/backup/export/route.ts @@ -8,15 +8,12 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { exportOrchestrationConfig } from '@/lib/orchestration/backup/exporter'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const payload = await exportOrchestrationConfig(); diff --git a/app/api/v1/admin/orchestration/backup/import/route.ts b/app/api/v1/admin/orchestration/backup/import/route.ts index 50218aa47..99715a789 100644 --- a/app/api/v1/admin/orchestration/backup/import/route.ts +++ b/app/api/v1/admin/orchestration/backup/import/route.ts @@ -9,7 +9,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse, errorResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { importOrchestrationConfig } from '@/lib/orchestration/backup/importer'; @@ -17,8 +16,6 @@ import { ZodError } from 'zod'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/capabilities/[id]/agents/route.ts b/app/api/v1/admin/orchestration/capabilities/[id]/agents/route.ts index 1ef3819f1..5dba7940a 100644 --- a/app/api/v1/admin/orchestration/capabilities/[id]/agents/route.ts +++ b/app/api/v1/admin/orchestration/capabilities/[id]/agents/route.ts @@ -20,15 +20,9 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/capabilities/[id]/route.ts b/app/api/v1/admin/orchestration/capabilities/[id]/route.ts index 119b22a77..7774ae087 100644 --- a/app/api/v1/admin/orchestration/capabilities/[id]/route.ts +++ b/app/api/v1/admin/orchestration/capabilities/[id]/route.ts @@ -21,7 +21,6 @@ import { successResponse } from '@/lib/api/responses'; import { ForbiddenError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; import { updateCapabilitySchema } from '@/lib/validations/orchestration'; @@ -50,8 +49,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -141,8 +138,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/capabilities/route.ts b/app/api/v1/admin/orchestration/capabilities/route.ts index 7537eae20..5378e131c 100644 --- a/app/api/v1/admin/orchestration/capabilities/route.ts +++ b/app/api/v1/admin/orchestration/capabilities/route.ts @@ -17,7 +17,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; import { @@ -77,8 +76,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createCapabilitySchema); diff --git a/app/api/v1/admin/orchestration/chat/stream/route.ts b/app/api/v1/admin/orchestration/chat/stream/route.ts index 31726e262..6383ca5f1 100644 --- a/app/api/v1/admin/orchestration/chat/stream/route.ts +++ b/app/api/v1/admin/orchestration/chat/stream/route.ts @@ -22,23 +22,17 @@ import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; import { prisma } from '@/lib/db/client'; import { - adminLimiter, agentChatLimiter, chatLimiter, createRateLimitResponse, imageLimiter, } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { streamChat } from '@/lib/orchestration/chat'; import { chatStreamRequestSchema } from '@/lib/validations/orchestration'; import { getRequestId } from '@/lib/logging/context'; import { validateImageMagicBytes, validatePdfMagicBytes } from '@/lib/storage/image'; export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const ipLimit = adminLimiter.check(clientIP); - if (!ipLimit.success) return createRateLimitResponse(ipLimit); - const userLimit = chatLimiter.check(session.user.id); if (!userLimit.success) return createRateLimitResponse(userLimit); diff --git a/app/api/v1/admin/orchestration/conversations/[id]/provenance.md/route.ts b/app/api/v1/admin/orchestration/conversations/[id]/provenance.md/route.ts index fa55f24de..8d598603f 100644 --- a/app/api/v1/admin/orchestration/conversations/[id]/provenance.md/route.ts +++ b/app/api/v1/admin/orchestration/conversations/[id]/provenance.md/route.ts @@ -22,7 +22,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { messageProvenanceSchema } from '@/lib/validations/orchestration'; @@ -34,10 +33,6 @@ import { logConversationAccess } from '@/lib/orchestration/audit/admin-audit-log import { adminCanViewConversation } from '@/lib/orchestration/access/conversation-access'; export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/conversations/[id]/provenance/route.ts b/app/api/v1/admin/orchestration/conversations/[id]/provenance/route.ts index 788a28a11..0f71116da 100644 --- a/app/api/v1/admin/orchestration/conversations/[id]/provenance/route.ts +++ b/app/api/v1/admin/orchestration/conversations/[id]/provenance/route.ts @@ -24,7 +24,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { messageProvenanceSchema } from '@/lib/validations/orchestration'; @@ -32,10 +31,6 @@ import { logConversationAccess } from '@/lib/orchestration/audit/admin-audit-log import { adminCanViewConversation } from '@/lib/orchestration/access/conversation-access'; export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/conversations/[id]/route.ts b/app/api/v1/admin/orchestration/conversations/[id]/route.ts index b0fc84fdf..bae217243 100644 --- a/app/api/v1/admin/orchestration/conversations/[id]/route.ts +++ b/app/api/v1/admin/orchestration/conversations/[id]/route.ts @@ -26,7 +26,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { updateConversationSchema } from '@/lib/validations/orchestration'; @@ -70,10 +69,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, session, { para }); export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); @@ -104,10 +99,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa }); export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/conversations/clear/route.ts b/app/api/v1/admin/orchestration/conversations/clear/route.ts index e80a82749..0089632d3 100644 --- a/app/api/v1/admin/orchestration/conversations/clear/route.ts +++ b/app/api/v1/admin/orchestration/conversations/clear/route.ts @@ -27,15 +27,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { clearConversationsBodySchema } from '@/lib/validations/orchestration'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, clearConversationsBodySchema); diff --git a/app/api/v1/admin/orchestration/costs/alerts/route.ts b/app/api/v1/admin/orchestration/costs/alerts/route.ts index 1270259df..85419b048 100644 --- a/app/api/v1/admin/orchestration/costs/alerts/route.ts +++ b/app/api/v1/admin/orchestration/costs/alerts/route.ts @@ -17,14 +17,8 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { getBudgetAlerts, getGlobalCapStatus } from '@/lib/orchestration/llm/cost-reports'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; export const GET = withAdminAuth(async (request) => { - const ip = getClientIP(request); - const rl = adminLimiter.check(ip); - if (!rl.success) return createRateLimitResponse(rl); - const log = await getRouteLogger(request); const [alerts, globalCap] = await Promise.all([getBudgetAlerts(), getGlobalCapStatus()]); log.info('Budget alerts fetched', { diff --git a/app/api/v1/admin/orchestration/costs/route.ts b/app/api/v1/admin/orchestration/costs/route.ts index a599e6f9d..253c3dfbc 100644 --- a/app/api/v1/admin/orchestration/costs/route.ts +++ b/app/api/v1/admin/orchestration/costs/route.ts @@ -16,23 +16,19 @@ * The schema enforces `dateTo >= dateFrom` and a 366-day maximum span * to prevent unbounded scans. * - * Authentication: Admin role required. Rate-limited via `adminLimiter`. + * Authentication: Admin role required. Rate limiting is enforced centrally + * by `proxy.ts` via the policy table at `lib/security/rate-limit-policy.ts` + * (orchestration tier — see `.context/security/rate-limiting.md`). */ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { costBreakdownQuerySchema } from '@/lib/validations/orchestration'; import { getCostBreakdown } from '@/lib/orchestration/llm/cost-reports'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const params = validateQueryParams(searchParams, costBreakdownQuerySchema); diff --git a/app/api/v1/admin/orchestration/costs/summary/route.ts b/app/api/v1/admin/orchestration/costs/summary/route.ts index 8c90fb199..03c735ccb 100644 --- a/app/api/v1/admin/orchestration/costs/summary/route.ts +++ b/app/api/v1/admin/orchestration/costs/summary/route.ts @@ -19,14 +19,8 @@ import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { getCostSummary } from '@/lib/orchestration/llm/cost-reports'; import { computeETag, checkConditional } from '@/lib/api/etag'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const summary = await getCostSummary(); diff --git a/app/api/v1/admin/orchestration/discovery/models/route.ts b/app/api/v1/admin/orchestration/discovery/models/route.ts index b54256894..f48240db1 100644 --- a/app/api/v1/admin/orchestration/discovery/models/route.ts +++ b/app/api/v1/admin/orchestration/discovery/models/route.ts @@ -30,8 +30,6 @@ import { prisma } from '@/lib/db/client'; import { errorResponse, successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getProvider, isApiKeyEnvVarSet } from '@/lib/orchestration/llm/provider-manager'; import { inferCapability, type Capability } from '@/lib/orchestration/llm/capability-inference'; import { getModelsByProvider, refreshFromOpenRouter } from '@/lib/orchestration/llm/model-registry'; @@ -76,10 +74,6 @@ interface DiscoveryCandidate { } export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const url = new URL(request.url); const parsed = querySchema.safeParse({ diff --git a/app/api/v1/admin/orchestration/embedding-models/route.ts b/app/api/v1/admin/orchestration/embedding-models/route.ts index 339531072..2d8cdf502 100644 --- a/app/api/v1/admin/orchestration/embedding-models/route.ts +++ b/app/api/v1/admin/orchestration/embedding-models/route.ts @@ -14,8 +14,6 @@ import { z } from 'zod'; import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getEmbeddingModels } from '@/lib/orchestration/llm/embedding-models'; const booleanQueryParam = z @@ -33,10 +31,6 @@ const embeddingModelsQuerySchema = z.object({ }); export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { searchParams } = new URL(request.url); const { schemaCompatibleOnly, hasFreeTier, local } = validateQueryParams( searchParams, diff --git a/app/api/v1/admin/orchestration/evaluations/[id]/complete/route.ts b/app/api/v1/admin/orchestration/evaluations/[id]/complete/route.ts index c6ddc774e..8f1c8d3b8 100644 --- a/app/api/v1/admin/orchestration/evaluations/[id]/complete/route.ts +++ b/app/api/v1/admin/orchestration/evaluations/[id]/complete/route.ts @@ -23,17 +23,11 @@ import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { completeEvaluationBodySchema } from '@/lib/validations/orchestration'; import { completeEvaluationSession } from '@/lib/orchestration/evaluations'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/evaluations/[id]/rescore/route.ts b/app/api/v1/admin/orchestration/evaluations/[id]/rescore/route.ts index 9e9c255f2..beebbf3c8 100644 --- a/app/api/v1/admin/orchestration/evaluations/[id]/rescore/route.ts +++ b/app/api/v1/admin/orchestration/evaluations/[id]/rescore/route.ts @@ -21,17 +21,11 @@ import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { rescoreEvaluationBodySchema } from '@/lib/validations/orchestration'; import { rescoreEvaluationSession } from '@/lib/orchestration/evaluations'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/evaluations/[id]/route.ts b/app/api/v1/admin/orchestration/evaluations/[id]/route.ts index ca1e04299..fb7d18a58 100644 --- a/app/api/v1/admin/orchestration/evaluations/[id]/route.ts +++ b/app/api/v1/admin/orchestration/evaluations/[id]/route.ts @@ -21,8 +21,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { updateEvaluationSchema } from '@/lib/validations/orchestration'; @@ -57,10 +55,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, session, { para }); export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseId(rawId); diff --git a/app/api/v1/admin/orchestration/evaluations/route.ts b/app/api/v1/admin/orchestration/evaluations/route.ts index 15bf0ef1f..699415e9a 100644 --- a/app/api/v1/admin/orchestration/evaluations/route.ts +++ b/app/api/v1/admin/orchestration/evaluations/route.ts @@ -17,8 +17,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { createEvaluationSchema, listEvaluationsQuerySchema, @@ -60,10 +58,6 @@ export const GET = withAdminAuth(async (request, session) => { }); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, createEvaluationSchema); diff --git a/app/api/v1/admin/orchestration/executions/[id]/approve/route.ts b/app/api/v1/admin/orchestration/executions/[id]/approve/route.ts index 70f44c9d3..04c55f33a 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/approve/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/approve/route.ts @@ -19,8 +19,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError, ConflictError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { logger } from '@/lib/logging'; import { approveExecutionBodySchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; @@ -29,10 +27,6 @@ import { isApproverInTrace } from '@/lib/orchestration/approval-scoping'; import { resumeApprovedExecution } from '@/lib/orchestration/scheduling'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/[id]/cancel/route.ts b/app/api/v1/admin/orchestration/executions/[id]/cancel/route.ts index fd01d4762..69fc08878 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/cancel/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/cancel/route.ts @@ -18,8 +18,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { isApproverInTrace } from '@/lib/orchestration/approval-scoping'; import { WorkflowStatus } from '@/types/orchestration'; @@ -30,10 +28,6 @@ const CANCELLABLE_STATUSES = new Set([ ]); export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.ts b/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.ts index 6a4f7abb6..3e5c45f02 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.ts @@ -33,7 +33,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -53,8 +52,6 @@ const ForceFailBodySchema = z.object({ export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/executions/[id]/lease/route.ts b/app/api/v1/admin/orchestration/executions/[id]/lease/route.ts index 77900d208..1ae67d455 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/lease/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/lease/route.ts @@ -22,18 +22,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { redactLeaseToken } from '@/lib/orchestration/engine/lease'; const HISTORY_LIMIT = 50; export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsedId = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/executions/[id]/live/route.ts b/app/api/v1/admin/orchestration/executions/[id]/live/route.ts index 1169c505b..1f4909b1e 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/live/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/live/route.ts @@ -33,8 +33,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { executionTraceSchema } from '@/lib/validations/orchestration'; @@ -51,11 +49,7 @@ interface CostEntry { createdAt: string; } -export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/[id]/reject/route.ts b/app/api/v1/admin/orchestration/executions/[id]/reject/route.ts index 12599de2a..54e063ff2 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/reject/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/reject/route.ts @@ -19,18 +19,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError, ConflictError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { rejectExecutionBodySchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; import { executeRejection } from '@/lib/orchestration/approval-actions'; import { isApproverInTrace } from '@/lib/orchestration/approval-scoping'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/[id]/report.md/route.ts b/app/api/v1/admin/orchestration/executions/[id]/report.md/route.ts index 3cbfb34d1..fe6031f49 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/report.md/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/report.md/route.ts @@ -19,8 +19,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { logger } from '@/lib/logging'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { executionTraceSchema, supervisorReportSchema } from '@/lib/validations/orchestration'; import { @@ -41,11 +39,7 @@ const TERMINAL_STATUSES = new Set([ WorkflowStatus.CANCELLED, ]); -export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/[id]/rerun/route.ts b/app/api/v1/admin/orchestration/executions/[id]/rerun/route.ts index 069fc5b72..79a022184 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/rerun/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/rerun/route.ts @@ -34,8 +34,6 @@ import { prisma } from '@/lib/db/client'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { sseResponse } from '@/lib/api/sse'; import { OrchestrationEngine } from '@/lib/orchestration/engine/orchestration-engine'; import { rerunExecutionBodySchema } from '@/lib/validations/orchestration'; @@ -46,10 +44,6 @@ import { } from '@/app/api/v1/admin/orchestration/workflows/[id]/_shared/execute-helpers'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/executions/[id]/retry-step/route.ts b/app/api/v1/admin/orchestration/executions/[id]/retry-step/route.ts index 345692c2e..f255c399b 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/retry-step/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/retry-step/route.ts @@ -23,17 +23,11 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { retryStepBodySchema, executionTraceSchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; import { WorkflowStatus } from '@/types/orchestration'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/executions/[id]/review/route.ts b/app/api/v1/admin/orchestration/executions/[id]/review/route.ts index 8a698dfff..5d95bfac3 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/review/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/review/route.ts @@ -31,8 +31,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; import { logger } from '@/lib/logging'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { executionTraceSchema, priorSupervisorReportSchema } from '@/lib/validations/orchestration'; import { @@ -93,10 +91,6 @@ function stepOutputsFromTrace(trace: ExecutionTraceEntry[]): Record(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsedId = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/executions/[id]/route.ts b/app/api/v1/admin/orchestration/executions/[id]/route.ts index c73b75ab1..83d60ad37 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/route.ts @@ -25,8 +25,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { executionTraceSchema } from '@/lib/validations/orchestration'; import { overlayStepDescriptions } from '@/lib/orchestration/trace/overlay-descriptions'; @@ -52,11 +50,7 @@ interface CostEntry { createdAt: string; } -export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/[id]/status/route.ts b/app/api/v1/admin/orchestration/executions/[id]/status/route.ts index bba5cacb3..a28fa3a85 100644 --- a/app/api/v1/admin/orchestration/executions/[id]/status/route.ts +++ b/app/api/v1/admin/orchestration/executions/[id]/status/route.ts @@ -17,15 +17,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; -export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/admin/orchestration/executions/counts/route.ts b/app/api/v1/admin/orchestration/executions/counts/route.ts index 8787b3ef6..15150364c 100644 --- a/app/api/v1/admin/orchestration/executions/counts/route.ts +++ b/app/api/v1/admin/orchestration/executions/counts/route.ts @@ -21,16 +21,10 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { executionCountsQuerySchema } from '@/lib/validations/orchestration'; import type { WorkflowStatus } from '@/types/orchestration'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const { statuses } = validateQueryParams(searchParams, executionCountsQuerySchema); diff --git a/app/api/v1/admin/orchestration/executions/live/route.ts b/app/api/v1/admin/orchestration/executions/live/route.ts index d4afc149f..adb67cd41 100644 --- a/app/api/v1/admin/orchestration/executions/live/route.ts +++ b/app/api/v1/admin/orchestration/executions/live/route.ts @@ -15,15 +15,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getLiveEngineSnapshot } from '@/lib/orchestration/admin/live-engine-snapshot'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); // User-scope the counts so they match the executions list, the // force-fail / lease / cancel routes — all of which are scoped to diff --git a/app/api/v1/admin/orchestration/executions/route.ts b/app/api/v1/admin/orchestration/executions/route.ts index 3168759bd..857f68103 100644 --- a/app/api/v1/admin/orchestration/executions/route.ts +++ b/app/api/v1/admin/orchestration/executions/route.ts @@ -15,16 +15,10 @@ import { prisma } from '@/lib/db/client'; import { paginatedResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { listExecutionsQuerySchema } from '@/lib/validations/orchestration'; import { WorkflowStatus } from '@/types/orchestration'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const { page, limit, workflowId, status, startDate, endDate } = validateQueryParams( diff --git a/app/api/v1/admin/orchestration/experiments/[id]/route.ts b/app/api/v1/admin/orchestration/experiments/[id]/route.ts index d552bf609..dac8edc4d 100644 --- a/app/api/v1/admin/orchestration/experiments/[id]/route.ts +++ b/app/api/v1/admin/orchestration/experiments/[id]/route.ts @@ -14,7 +14,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -61,8 +60,6 @@ export const GET = withAdminAuth(async (request, _session, { params }) = export const PATCH = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id } = await params; const log = await getRouteLogger(request); @@ -112,8 +109,6 @@ export const PATCH = withAdminAuth(async (request, session, { params }) export const DELETE = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id } = await params; const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/experiments/[id]/run/route.ts b/app/api/v1/admin/orchestration/experiments/[id]/run/route.ts index 89016fd24..4888f9ca9 100644 --- a/app/api/v1/admin/orchestration/experiments/[id]/run/route.ts +++ b/app/api/v1/admin/orchestration/experiments/[id]/run/route.ts @@ -15,7 +15,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -24,8 +23,6 @@ type Params = { id: string }; export const POST = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id } = await params; const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/experiments/route.ts b/app/api/v1/admin/orchestration/experiments/route.ts index 3829ccce0..f99c9dd83 100644 --- a/app/api/v1/admin/orchestration/experiments/route.ts +++ b/app/api/v1/admin/orchestration/experiments/route.ts @@ -13,7 +13,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { validateRequestBody, validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { paginationQuerySchema } from '@/lib/validations/common'; @@ -39,10 +38,6 @@ const createSchema = z.object({ }); export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const query = validateQueryParams(searchParams, listSchema); @@ -78,8 +73,6 @@ export const GET = withAdminAuth(async (request) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createSchema); diff --git a/app/api/v1/admin/orchestration/hooks/[id]/deliveries/route.ts b/app/api/v1/admin/orchestration/hooks/[id]/deliveries/route.ts index 19b40480e..58a591abe 100644 --- a/app/api/v1/admin/orchestration/hooks/[id]/deliveries/route.ts +++ b/app/api/v1/admin/orchestration/hooks/[id]/deliveries/route.ts @@ -15,8 +15,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { paginatedResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; const querySchema = z.object({ @@ -27,10 +25,6 @@ const querySchema = z.object({ export const GET = withAdminAuth<{ id: string }>( async (request: NextRequest, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const idParsed = cuidSchema.safeParse(rawId); if (!idParsed.success) throw new ValidationError('Invalid hook ID format'); diff --git a/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.ts b/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.ts index 16c89d017..59afeb4f3 100644 --- a/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.ts +++ b/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.ts @@ -19,7 +19,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { generateHookSecret } from '@/lib/orchestration/hooks/signing'; @@ -28,8 +27,6 @@ import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -81,8 +78,6 @@ export const POST = withAdminAuth<{ id: string }>(async (request, session, { par export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/hooks/[id]/route.ts b/app/api/v1/admin/orchestration/hooks/[id]/route.ts index 3df34e137..53be738a3 100644 --- a/app/api/v1/admin/orchestration/hooks/[id]/route.ts +++ b/app/api/v1/admin/orchestration/hooks/[id]/route.ts @@ -14,7 +14,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { toSafeHook } from '@/lib/orchestration/hooks/serialize'; @@ -60,10 +59,6 @@ function resolveHookId(rawId: string): string { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = resolveHookId(rawId); @@ -76,10 +71,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par }); export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = resolveHookId(rawId); @@ -118,10 +109,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa }); export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = resolveHookId(rawId); diff --git a/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.ts b/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.ts index 8725bd332..0356b5561 100644 --- a/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.ts +++ b/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.ts @@ -12,7 +12,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { retryHookDelivery } from '@/lib/orchestration/hooks/registry'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -20,8 +19,6 @@ import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id } = await params; if (!cuidSchema.safeParse(id).success) { diff --git a/app/api/v1/admin/orchestration/hooks/route.ts b/app/api/v1/admin/orchestration/hooks/route.ts index 0ce02b77c..2524225a2 100644 --- a/app/api/v1/admin/orchestration/hooks/route.ts +++ b/app/api/v1/admin/orchestration/hooks/route.ts @@ -13,7 +13,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { toSafeHook } from '@/lib/orchestration/hooks/serialize'; @@ -45,10 +44,6 @@ const createHookSchema = z.object({ }); export const GET = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const page = Math.max(1, Number(searchParams.get('page') ?? 1)); @@ -73,8 +68,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { name, eventType, action, filter, isEnabled } = await validateRequestBody( diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/chunks/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/chunks/route.ts index 387b81907..c3428b22b 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/chunks/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/chunks/route.ts @@ -14,8 +14,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; const documentMetaSchema = z @@ -34,10 +32,6 @@ const documentMetaSchema = z .nullable(); export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.ts index 3f5aab8c4..e43b54458 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.ts @@ -15,7 +15,6 @@ import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { confirmPreview } from '@/lib/orchestration/knowledge/document-manager'; import { confirmDocumentPreviewSchema } from '@/lib/validations/orchestration'; @@ -24,8 +23,6 @@ import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/enrich-keywords/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/enrich-keywords/route.ts index 06568089b..a04771fe4 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/enrich-keywords/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/enrich-keywords/route.ts @@ -20,7 +20,6 @@ import { prisma } from '@/lib/db/client'; import { errorResponse, successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -32,8 +31,6 @@ import { NoDefaultModelConfiguredError } from '@/lib/orchestration/llm/settings- export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/rechunk/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/rechunk/route.ts index 7604d1655..5d042b268 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/rechunk/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/rechunk/route.ts @@ -15,7 +15,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { rechunkDocument } from '@/lib/orchestration/knowledge/document-manager'; import { cuidSchema } from '@/lib/validations/common'; @@ -23,8 +22,6 @@ import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/retry/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/retry/route.ts index c541dfaeb..34935df71 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/retry/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/retry/route.ts @@ -14,7 +14,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { rechunkDocument } from '@/lib/orchestration/knowledge/document-manager'; import { cuidSchema } from '@/lib/validations/common'; @@ -22,8 +21,6 @@ import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/documents/[id]/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/[id]/route.ts index acffe79f4..5f920afb2 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/[id]/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/[id]/route.ts @@ -17,7 +17,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { deleteDocument } from '@/lib/orchestration/knowledge/document-manager'; import { cuidSchema } from '@/lib/validations/common'; @@ -57,8 +56,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -121,8 +118,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/documents/bulk/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/bulk/route.ts index 43bd1835f..a96d6ed9d 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/bulk/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/bulk/route.ts @@ -15,7 +15,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { uploadDocument, @@ -52,8 +51,6 @@ interface FileResult { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/knowledge/documents/fetch-url/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/fetch-url/route.ts index 603c6ae58..dad804c49 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/fetch-url/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/fetch-url/route.ts @@ -15,7 +15,6 @@ import { prisma } from '@/lib/db/client'; import { errorResponse, successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { fetchDocumentFromUrl } from '@/lib/orchestration/knowledge/url-fetcher'; import { @@ -35,8 +34,6 @@ const fetchUrlSchema = z.object({ export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, fetchUrlSchema); diff --git a/app/api/v1/admin/orchestration/knowledge/documents/route.ts b/app/api/v1/admin/orchestration/knowledge/documents/route.ts index 8426a00b9..0b85488dd 100644 --- a/app/api/v1/admin/orchestration/knowledge/documents/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/documents/route.ts @@ -22,7 +22,6 @@ import { ValidationError } from '@/lib/api/errors'; import { enforceContentLengthCap } from '@/lib/api/multipart-guard'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { uploadDocument, @@ -184,8 +183,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/knowledge/embed/route.ts b/app/api/v1/admin/orchestration/knowledge/embed/route.ts index cbfd04844..bccc63e72 100644 --- a/app/api/v1/admin/orchestration/knowledge/embed/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/embed/route.ts @@ -16,15 +16,12 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { embedChunks } from '@/lib/orchestration/knowledge/seeder'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/knowledge/embedding-status/route.ts b/app/api/v1/admin/orchestration/knowledge/embedding-status/route.ts index 87910e3d0..82ded8da5 100644 --- a/app/api/v1/admin/orchestration/knowledge/embedding-status/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/embedding-status/route.ts @@ -11,15 +11,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { prisma } from '@/lib/db/client'; -export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth(async (_request) => { const [total, embeddedRows, hasProvider] = await Promise.all([ prisma.aiKnowledgeChunk.count(), prisma.$queryRaw<[{ count: bigint }]>` diff --git a/app/api/v1/admin/orchestration/knowledge/embeddings/route.ts b/app/api/v1/admin/orchestration/knowledge/embeddings/route.ts index 01a80ac7a..191da70cf 100644 --- a/app/api/v1/admin/orchestration/knowledge/embeddings/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/embeddings/route.ts @@ -44,8 +44,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; /** * Hard cap on how many chunks we'll project per request. Above this @@ -129,10 +127,6 @@ interface RawChunkRow { } export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const { scope, limit, nNeighbors } = validateQueryParams(searchParams, querySchema); diff --git a/app/api/v1/admin/orchestration/knowledge/graph/route.ts b/app/api/v1/admin/orchestration/knowledge/graph/route.ts index fef55d577..3890587e2 100644 --- a/app/api/v1/admin/orchestration/knowledge/graph/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/graph/route.ts @@ -20,8 +20,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import type { GraphNode, GraphLink, GraphCategory, GraphStats } from '@/types/orchestration'; /** Threshold for showing individual chunk nodes */ @@ -52,10 +50,6 @@ const graphQuerySchema = z.object({ }); export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); diff --git a/app/api/v1/admin/orchestration/knowledge/patterns/[number]/route.ts b/app/api/v1/admin/orchestration/knowledge/patterns/[number]/route.ts index c479d2296..a545a76e8 100644 --- a/app/api/v1/admin/orchestration/knowledge/patterns/[number]/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/patterns/[number]/route.ts @@ -13,16 +13,10 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getPatternDetail } from '@/lib/orchestration/knowledge/search'; import { getPatternParamSchema } from '@/lib/validations/orchestration'; export const GET = withAdminAuth<{ number: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { number: rawNumber } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/patterns/route.ts b/app/api/v1/admin/orchestration/knowledge/patterns/route.ts index b7ced17da..807607bf1 100644 --- a/app/api/v1/admin/orchestration/knowledge/patterns/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/patterns/route.ts @@ -12,15 +12,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { listPatterns } from '@/lib/orchestration/knowledge/search'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const patterns = await listPatterns(); diff --git a/app/api/v1/admin/orchestration/knowledge/search/route.ts b/app/api/v1/admin/orchestration/knowledge/search/route.ts index d0bbd5255..b6f2489f1 100644 --- a/app/api/v1/admin/orchestration/knowledge/search/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/search/route.ts @@ -13,17 +13,11 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { searchKnowledge, type SearchFilters } from '@/lib/orchestration/knowledge/search'; import { resolveAgentDocumentAccess } from '@/lib/orchestration/knowledge/resolveAgentDocumentAccess'; import { knowledgeSearchSchema } from '@/lib/validations/orchestration'; export const POST = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, knowledgeSearchSchema); diff --git a/app/api/v1/admin/orchestration/knowledge/seed/route.ts b/app/api/v1/admin/orchestration/knowledge/seed/route.ts index 6e0617657..7f0528fc2 100644 --- a/app/api/v1/admin/orchestration/knowledge/seed/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/seed/route.ts @@ -20,15 +20,12 @@ import path from 'path'; import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { seedChunks } from '@/lib/orchestration/knowledge/seeder'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/knowledge/tags/[id]/route.ts b/app/api/v1/admin/orchestration/knowledge/tags/[id]/route.ts index 50dd1129e..5afda0056 100644 --- a/app/api/v1/admin/orchestration/knowledge/tags/[id]/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/tags/[id]/route.ts @@ -28,7 +28,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { updateKnowledgeTagSchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; @@ -92,8 +91,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -146,8 +143,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/knowledge/tags/route.ts b/app/api/v1/admin/orchestration/knowledge/tags/route.ts index be63a49e5..1c8d08396 100644 --- a/app/api/v1/admin/orchestration/knowledge/tags/route.ts +++ b/app/api/v1/admin/orchestration/knowledge/tags/route.ts @@ -18,7 +18,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { createKnowledgeTagSchema, @@ -70,8 +69,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createKnowledgeTagSchema); diff --git a/app/api/v1/admin/orchestration/maintenance/tick/route.ts b/app/api/v1/admin/orchestration/maintenance/tick/route.ts index f6690326f..207882d80 100644 --- a/app/api/v1/admin/orchestration/maintenance/tick/route.ts +++ b/app/api/v1/admin/orchestration/maintenance/tick/route.ts @@ -28,8 +28,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { logger } from '@/lib/logging'; import { processDueSchedules, @@ -78,11 +76,7 @@ const BACKGROUND_TASK_NAMES = [ */ const BACKGROUND_TASK_MAX_MS = 5 * 60 * 1000; -export const POST = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const POST = withAdminAuth(async (_request) => { if (tickRunning) { logger.info('Maintenance tick skipped — previous tick still running'); return successResponse({ skipped: true, reason: 'previous tick still running' }); diff --git a/app/api/v1/admin/orchestration/mcp/audit/route.ts b/app/api/v1/admin/orchestration/mcp/audit/route.ts index 3da7ad178..077a65dba 100644 --- a/app/api/v1/admin/orchestration/mcp/audit/route.ts +++ b/app/api/v1/admin/orchestration/mcp/audit/route.ts @@ -10,16 +10,10 @@ import { prisma } from '@/lib/db/client'; import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { queryMcpAuditLogs, getMcpServerConfig } from '@/lib/orchestration/mcp'; import { mcpAuditQuerySchema } from '@/lib/validations/mcp'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const filters = validateQueryParams(new URL(request.url).searchParams, mcpAuditQuerySchema); @@ -38,10 +32,6 @@ export const GET = withAdminAuth(async (request) => { * Uses `auditRetentionDays` from McpServerConfig. Returns 0 if retention is disabled (0 days). */ export const DELETE = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const config = await getMcpServerConfig(); diff --git a/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.ts b/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.ts index 01d90b791..9b56e9d86 100644 --- a/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.ts +++ b/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.ts @@ -19,7 +19,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { generateApiKey } from '@/lib/orchestration/mcp/auth'; import { mcpApiKeyRotateSchema } from '@/lib/validations/mcp'; @@ -45,8 +44,6 @@ export const POST = withAdminAuth<{ id: string }>(async (request, session, { par cuidSchema.parse(id); const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/mcp/keys/[id]/route.ts b/app/api/v1/admin/orchestration/mcp/keys/[id]/route.ts index 94397957d..bd374fca2 100644 --- a/app/api/v1/admin/orchestration/mcp/keys/[id]/route.ts +++ b/app/api/v1/admin/orchestration/mcp/keys/[id]/route.ts @@ -11,7 +11,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { updateApiKeySchema } from '@/lib/validations/mcp'; import { cuidSchema } from '@/lib/validations/common'; @@ -22,8 +21,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa cuidSchema.parse(id); const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, updateApiKeySchema); @@ -73,8 +70,6 @@ export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { p cuidSchema.parse(id); const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/mcp/keys/route.ts b/app/api/v1/admin/orchestration/mcp/keys/route.ts index b8efb2c77..54c26a4f0 100644 --- a/app/api/v1/admin/orchestration/mcp/keys/route.ts +++ b/app/api/v1/admin/orchestration/mcp/keys/route.ts @@ -10,17 +10,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { validateRequestBody, validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { generateApiKey } from '@/lib/orchestration/mcp'; import { createApiKeySchema, listApiKeysQuerySchema } from '@/lib/validations/mcp'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { page, limit, isActive } = validateQueryParams( new URL(request.url).searchParams, @@ -62,8 +57,6 @@ export const GET = withAdminAuth(async (request) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createApiKeySchema); diff --git a/app/api/v1/admin/orchestration/mcp/resources/[id]/route.ts b/app/api/v1/admin/orchestration/mcp/resources/[id]/route.ts index 86264ee93..a69743ff7 100644 --- a/app/api/v1/admin/orchestration/mcp/resources/[id]/route.ts +++ b/app/api/v1/admin/orchestration/mcp/resources/[id]/route.ts @@ -11,8 +11,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { Prisma } from '@prisma/client'; import { clearMcpResourceCache, broadcastMcpResourcesChanged } from '@/lib/orchestration/mcp'; import { updateExposedResourceSchema } from '@/lib/validations/mcp'; @@ -22,10 +20,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa const { id } = await params; cuidSchema.parse(id); - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, updateExposedResourceSchema); @@ -59,10 +53,6 @@ export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { p const { id } = await params; cuidSchema.parse(id); - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const existing = await prisma.mcpExposedResource.findUnique({ where: { id } }); diff --git a/app/api/v1/admin/orchestration/mcp/resources/route.ts b/app/api/v1/admin/orchestration/mcp/resources/route.ts index b1100c1ff..9c9bdfc16 100644 --- a/app/api/v1/admin/orchestration/mcp/resources/route.ts +++ b/app/api/v1/admin/orchestration/mcp/resources/route.ts @@ -10,8 +10,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { validateRequestBody, validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { Prisma } from '@prisma/client'; import { clearMcpResourceCache, broadcastMcpResourcesChanged } from '@/lib/orchestration/mcp'; import { @@ -20,10 +18,6 @@ import { } from '@/lib/validations/mcp'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { page, limit, isEnabled, resourceType } = validateQueryParams( new URL(request.url).searchParams, @@ -51,10 +45,6 @@ export const GET = withAdminAuth(async (request) => { }); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, createExposedResourceSchema); diff --git a/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.ts b/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.ts index b69b5c706..0b7473959 100644 --- a/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.ts +++ b/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.ts @@ -8,17 +8,11 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getMcpSessionManager } from '@/lib/orchestration/mcp'; export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const { id } = await params; - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const sessionManager = getMcpSessionManager(); const destroyed = sessionManager.destroySession(id); diff --git a/app/api/v1/admin/orchestration/mcp/sessions/route.ts b/app/api/v1/admin/orchestration/mcp/sessions/route.ts index af5996e59..60e20a7d1 100644 --- a/app/api/v1/admin/orchestration/mcp/sessions/route.ts +++ b/app/api/v1/admin/orchestration/mcp/sessions/route.ts @@ -7,15 +7,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getMcpSessionManager } from '@/lib/orchestration/mcp'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const sessions = getMcpSessionManager().getActiveSessions(); diff --git a/app/api/v1/admin/orchestration/mcp/settings/route.ts b/app/api/v1/admin/orchestration/mcp/settings/route.ts index e061a0381..841249ae6 100644 --- a/app/api/v1/admin/orchestration/mcp/settings/route.ts +++ b/app/api/v1/admin/orchestration/mcp/settings/route.ts @@ -10,17 +10,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { getMcpServerConfig, invalidateMcpConfigCache } from '@/lib/orchestration/mcp'; import { logAdminAction, computeChanges } from '@/lib/orchestration/audit/admin-audit-logger'; import { updateMcpSettingsSchema } from '@/lib/validations/mcp'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const config = await getMcpServerConfig(); @@ -30,8 +25,6 @@ export const GET = withAdminAuth(async (request) => { export const PATCH = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, updateMcpSettingsSchema); diff --git a/app/api/v1/admin/orchestration/mcp/tools/[id]/route.ts b/app/api/v1/admin/orchestration/mcp/tools/[id]/route.ts index 3556f5fa9..aaebd18f0 100644 --- a/app/api/v1/admin/orchestration/mcp/tools/[id]/route.ts +++ b/app/api/v1/admin/orchestration/mcp/tools/[id]/route.ts @@ -11,8 +11,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { clearMcpToolCache, broadcastMcpToolsChanged } from '@/lib/orchestration/mcp'; import { updateExposedToolSchema } from '@/lib/validations/mcp'; import { cuidSchema } from '@/lib/validations/common'; @@ -21,10 +19,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa const { id } = await params; cuidSchema.parse(id); - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, updateExposedToolSchema); @@ -53,10 +47,6 @@ export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { p const { id } = await params; cuidSchema.parse(id); - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const existing = await prisma.mcpExposedTool.findUnique({ where: { id } }); diff --git a/app/api/v1/admin/orchestration/mcp/tools/route.ts b/app/api/v1/admin/orchestration/mcp/tools/route.ts index 944de154d..388509897 100644 --- a/app/api/v1/admin/orchestration/mcp/tools/route.ts +++ b/app/api/v1/admin/orchestration/mcp/tools/route.ts @@ -10,16 +10,10 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { validateRequestBody, validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { clearMcpToolCache, broadcastMcpToolsChanged } from '@/lib/orchestration/mcp'; import { createExposedToolSchema, listExposedToolsQuerySchema } from '@/lib/validations/mcp'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { page, limit, isEnabled } = validateQueryParams( new URL(request.url).searchParams, @@ -47,10 +41,6 @@ export const GET = withAdminAuth(async (request) => { }); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, createExposedToolSchema); diff --git a/app/api/v1/admin/orchestration/models/route.ts b/app/api/v1/admin/orchestration/models/route.ts index d97ca7700..3b6d5c717 100644 --- a/app/api/v1/admin/orchestration/models/route.ts +++ b/app/api/v1/admin/orchestration/models/route.ts @@ -19,8 +19,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { mergeDbModelsWithRegistry } from '@/lib/orchestration/llm/db-model-adapter'; import { getAvailableModels, @@ -34,9 +32,6 @@ export const GET = withAdminAuth(async (request, _session) => { const refresh = searchParams.get('refresh') === 'true'; if (refresh) { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); await refreshFromOpenRouter({ force: true }); log.info('Model registry refresh forced'); } else { diff --git a/app/api/v1/admin/orchestration/observability/dashboard-stats/route.ts b/app/api/v1/admin/orchestration/observability/dashboard-stats/route.ts index b9476ce56..22937d2b5 100644 --- a/app/api/v1/admin/orchestration/observability/dashboard-stats/route.ts +++ b/app/api/v1/admin/orchestration/observability/dashboard-stats/route.ts @@ -20,14 +20,8 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { computeETag, checkConditional } from '@/lib/api/etag'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const now = new Date(); diff --git a/app/api/v1/admin/orchestration/provider-models/[id]/route.ts b/app/api/v1/admin/orchestration/provider-models/[id]/route.ts index 0a220ad3d..1afcf00ea 100644 --- a/app/api/v1/admin/orchestration/provider-models/[id]/route.ts +++ b/app/api/v1/admin/orchestration/provider-models/[id]/route.ts @@ -17,8 +17,6 @@ import { errorResponse, successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { invalidateModelCache } from '@/lib/orchestration/llm/provider-selector'; import { updateProviderModelSchema } from '@/lib/validations/orchestration'; import { cuidSchema } from '@/lib/validations/common'; @@ -54,10 +52,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par }); export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseModelId(rawId); @@ -129,10 +123,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa }); export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseModelId(rawId); diff --git a/app/api/v1/admin/orchestration/provider-models/bulk/route.ts b/app/api/v1/admin/orchestration/provider-models/bulk/route.ts index 06c00aa80..078561682 100644 --- a/app/api/v1/admin/orchestration/provider-models/bulk/route.ts +++ b/app/api/v1/admin/orchestration/provider-models/bulk/route.ts @@ -25,8 +25,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { getRouteLogger } from '@/lib/api/context'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { invalidateModelCache } from '@/lib/orchestration/llm/provider-selector'; import { deriveMatrixSlug } from '@/lib/orchestration/llm/model-heuristics'; import { bulkCreateProviderModelsSchema } from '@/lib/validations/orchestration'; @@ -46,10 +44,6 @@ interface ConflictRow { } export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, bulkCreateProviderModelsSchema); diff --git a/app/api/v1/admin/orchestration/provider-models/route.ts b/app/api/v1/admin/orchestration/provider-models/route.ts index 565c046b7..40989155d 100644 --- a/app/api/v1/admin/orchestration/provider-models/route.ts +++ b/app/api/v1/admin/orchestration/provider-models/route.ts @@ -17,7 +17,7 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; +import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { invalidateModelCache } from '@/lib/orchestration/llm/provider-selector'; import { parseAudioDefault } from '@/lib/orchestration/llm/audio-default'; @@ -163,10 +163,6 @@ export const GET = withAdminAuth(async (request, _session) => { }); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const body = await validateRequestBody(request, createProviderModelSchema); diff --git a/app/api/v1/admin/orchestration/providers/[id]/health/route.ts b/app/api/v1/admin/orchestration/providers/[id]/health/route.ts index 04918eadd..42170cbe6 100644 --- a/app/api/v1/admin/orchestration/providers/[id]/health/route.ts +++ b/app/api/v1/admin/orchestration/providers/[id]/health/route.ts @@ -12,7 +12,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { getBreaker, getCircuitBreakerStatus } from '@/lib/orchestration/llm/circuit-breaker'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -54,8 +53,6 @@ export const POST = withAdminAuth<{ id: string }>(async (request, session, { par const { id: rawId } = await params; const id = parseProviderId(rawId); const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); diff --git a/app/api/v1/admin/orchestration/providers/[id]/models/route.ts b/app/api/v1/admin/orchestration/providers/[id]/models/route.ts index 4e084f221..1ca89738a 100644 --- a/app/api/v1/admin/orchestration/providers/[id]/models/route.ts +++ b/app/api/v1/admin/orchestration/providers/[id]/models/route.ts @@ -27,14 +27,8 @@ import { getOrchestrationSettings } from '@/lib/orchestration/settings'; import { parseAudioDefault } from '@/lib/orchestration/llm/audio-default'; import { TASK_TYPES, type TaskType } from '@/types/orchestration'; import { cuidSchema } from '@/lib/validations/common'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/providers/[id]/route.ts b/app/api/v1/admin/orchestration/providers/[id]/route.ts index c820732b3..940781bcc 100644 --- a/app/api/v1/admin/orchestration/providers/[id]/route.ts +++ b/app/api/v1/admin/orchestration/providers/[id]/route.ts @@ -22,7 +22,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { clearCache as clearProviderCache, @@ -57,8 +56,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -126,8 +123,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/providers/[id]/test-model/route.ts b/app/api/v1/admin/orchestration/providers/[id]/test-model/route.ts index f42410c52..80e9e653f 100644 --- a/app/api/v1/admin/orchestration/providers/[id]/test-model/route.ts +++ b/app/api/v1/admin/orchestration/providers/[id]/test-model/route.ts @@ -17,8 +17,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getProvider } from '@/lib/orchestration/llm/provider-manager'; import { generateSilentWav } from '@/lib/audio/silent-wav'; import { deriveParamProfile } from '@/lib/orchestration/llm/model-heuristics'; @@ -62,10 +60,6 @@ const UNSUPPORTED_TEST_MESSAGES: Record = { }; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/providers/[id]/test/route.ts b/app/api/v1/admin/orchestration/providers/[id]/test/route.ts index 4c397f5a0..cbea25233 100644 --- a/app/api/v1/admin/orchestration/providers/[id]/test/route.ts +++ b/app/api/v1/admin/orchestration/providers/[id]/test/route.ts @@ -16,16 +16,10 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { testProvider } from '@/lib/orchestration/llm/provider-manager'; import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/providers/route.ts b/app/api/v1/admin/orchestration/providers/route.ts index 738bd4138..ee9050b49 100644 --- a/app/api/v1/admin/orchestration/providers/route.ts +++ b/app/api/v1/admin/orchestration/providers/route.ts @@ -16,7 +16,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { isApiKeyEnvVarSet, @@ -73,8 +72,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, providerConfigSchema); diff --git a/app/api/v1/admin/orchestration/providers/test-bulk/route.ts b/app/api/v1/admin/orchestration/providers/test-bulk/route.ts index fd04e1e17..08b5d22c5 100644 --- a/app/api/v1/admin/orchestration/providers/test-bulk/route.ts +++ b/app/api/v1/admin/orchestration/providers/test-bulk/route.ts @@ -22,8 +22,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { testProvider } from '@/lib/orchestration/llm/provider-manager'; import { bulkProviderTestSchema } from '@/lib/validations/orchestration'; @@ -39,10 +37,6 @@ interface BulkTestResult { } export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { providerIds } = await validateRequestBody(request, bulkProviderTestSchema); diff --git a/app/api/v1/admin/orchestration/quiz-scores/route.ts b/app/api/v1/admin/orchestration/quiz-scores/route.ts index a3d17a650..75c511f2a 100644 --- a/app/api/v1/admin/orchestration/quiz-scores/route.ts +++ b/app/api/v1/admin/orchestration/quiz-scores/route.ts @@ -16,8 +16,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { saveQuizScoreSchema } from '@/lib/validations/orchestration'; const quizMetadataSchema = z.object({ @@ -27,10 +25,6 @@ const quizMetadataSchema = z.object({ const QUIZ_MASTER_SLUG = 'quiz-master'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const sessions = await prisma.aiEvaluationSession.findMany({ @@ -64,10 +58,6 @@ export const GET = withAdminAuth(async (request, session) => { }); export const POST = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { correct, total } = await validateRequestBody(request, saveQuizScoreSchema); diff --git a/app/api/v1/admin/orchestration/schedules/tick/route.ts b/app/api/v1/admin/orchestration/schedules/tick/route.ts index 4e69314d1..20a0632cd 100644 --- a/app/api/v1/admin/orchestration/schedules/tick/route.ts +++ b/app/api/v1/admin/orchestration/schedules/tick/route.ts @@ -12,15 +12,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { processDueSchedules } from '@/lib/orchestration/scheduling'; -export const POST = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const POST = withAdminAuth(async (_request) => { const result = await processDueSchedules(); return successResponse({ ...result }); diff --git a/app/api/v1/admin/orchestration/settings/route.ts b/app/api/v1/admin/orchestration/settings/route.ts index 50da11eb5..e933b0a5b 100644 --- a/app/api/v1/admin/orchestration/settings/route.ts +++ b/app/api/v1/admin/orchestration/settings/route.ts @@ -9,9 +9,10 @@ * against the registry and invalidates the in-memory cache so the next chat * turn picks up the change. * - * Authentication: Admin role required. Both GET and PATCH are rate-limited via - * the shared `adminLimiter` — GET performs an upsert-on-read, so it is a - * mutating endpoint despite the HTTP verb. + * Authentication: Admin role required. Both GET and PATCH are rate-limited + * centrally by `proxy.ts` via the orchestration tier in the policy table at + * `lib/security/rate-limit-policy.ts`. GET performs an upsert-on-read, so it + * is a mutating endpoint despite the HTTP verb — the cap still applies. */ import { Prisma } from '@prisma/client'; @@ -20,7 +21,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse, errorResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { computeETag, checkConditional } from '@/lib/api/etag'; import { computeDefaultModelMap } from '@/lib/orchestration/llm/model-registry'; @@ -37,10 +37,6 @@ import { import { TASK_TYPES, type TaskType } from '@/types/orchestration'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const settings = await getOrchestrationSettings(); @@ -56,8 +52,6 @@ export const GET = withAdminAuth(async (request) => { export const PATCH = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, updateOrchestrationSettingsSchema); diff --git a/app/api/v1/admin/orchestration/webhooks/[id]/deliveries/route.ts b/app/api/v1/admin/orchestration/webhooks/[id]/deliveries/route.ts index e5136dee9..027415606 100644 --- a/app/api/v1/admin/orchestration/webhooks/[id]/deliveries/route.ts +++ b/app/api/v1/admin/orchestration/webhooks/[id]/deliveries/route.ts @@ -15,8 +15,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { paginatedResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; const querySchema = z.object({ @@ -27,10 +25,6 @@ const querySchema = z.object({ export const GET = withAdminAuth<{ id: string }>( async (request: NextRequest, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const idParsed = cuidSchema.safeParse(rawId); if (!idParsed.success) throw new ValidationError('Invalid webhook ID format'); diff --git a/app/api/v1/admin/orchestration/webhooks/[id]/route.ts b/app/api/v1/admin/orchestration/webhooks/[id]/route.ts index f11920e38..8783c6827 100644 --- a/app/api/v1/admin/orchestration/webhooks/[id]/route.ts +++ b/app/api/v1/admin/orchestration/webhooks/[id]/route.ts @@ -15,7 +15,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { updateWebhookSchema } from '@/lib/validations/orchestration'; @@ -32,11 +31,7 @@ const SAFE_SELECT = { updatedAt: true, } as const; -export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) @@ -52,10 +47,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, session, { para }); export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); @@ -98,10 +89,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa }); export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/webhooks/[id]/test/route.ts b/app/api/v1/admin/orchestration/webhooks/[id]/test/route.ts index 13b993161..112f91141 100644 --- a/app/api/v1/admin/orchestration/webhooks/[id]/test/route.ts +++ b/app/api/v1/admin/orchestration/webhooks/[id]/test/route.ts @@ -14,16 +14,10 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import crypto from 'crypto'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/webhooks/deliveries/[id]/retry/route.ts b/app/api/v1/admin/orchestration/webhooks/deliveries/[id]/retry/route.ts index 17d3ecb5b..3a464b6ba 100644 --- a/app/api/v1/admin/orchestration/webhooks/deliveries/[id]/retry/route.ts +++ b/app/api/v1/admin/orchestration/webhooks/deliveries/[id]/retry/route.ts @@ -13,7 +13,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { retryDelivery } from '@/lib/orchestration/webhooks/dispatcher'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -21,8 +20,6 @@ import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawId } = await params; if (!cuidSchema.safeParse(rawId).success) { diff --git a/app/api/v1/admin/orchestration/webhooks/route.ts b/app/api/v1/admin/orchestration/webhooks/route.ts index 165ec6801..2b43d97e2 100644 --- a/app/api/v1/admin/orchestration/webhooks/route.ts +++ b/app/api/v1/admin/orchestration/webhooks/route.ts @@ -13,16 +13,11 @@ import { prisma } from '@/lib/db/client'; import { successResponse, paginatedResponse } from '@/lib/api/responses'; import { validateRequestBody, validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { createWebhookSchema, listWebhooksQuerySchema } from '@/lib/validations/orchestration'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; export const GET = withAdminAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const { page, limit, isActive } = validateQueryParams(searchParams, listWebhooksQuerySchema); @@ -60,8 +55,6 @@ export const GET = withAdminAuth(async (request, session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createWebhookSchema); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/cost-estimate/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/cost-estimate/route.ts index 3f5861005..7beb3ab9e 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/cost-estimate/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/cost-estimate/route.ts @@ -31,8 +31,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { estimateWorkflowCost, @@ -85,10 +83,6 @@ async function resolveEffectiveCap(workflowId: string): Promise { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const idParsed = cuidSchema.safeParse(rawId); @@ -144,10 +138,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par * reflect the live draft, not the last-published snapshot. */ export const POST = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const idParsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/discard-draft/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/discard-draft/route.ts index 2d91791ea..2d798133f 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/discard-draft/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/discard-draft/route.ts @@ -12,15 +12,12 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { discardDraft } from '@/lib/orchestration/workflows/version-service'; import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/dry-run/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/dry-run/route.ts index 387c9a90e..b95353e4c 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/dry-run/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/dry-run/route.ts @@ -20,8 +20,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { validateWorkflow, semanticValidateWorkflow, @@ -34,10 +32,6 @@ import { } from '@/lib/validations/orchestration'; export const POST = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/execute-stream/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/execute-stream/route.ts index 828392e74..667239bdd 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/execute-stream/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/execute-stream/route.ts @@ -17,8 +17,6 @@ import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; import { sseResponse } from '@/lib/api/sse'; import { OrchestrationEngine } from '@/lib/orchestration/engine/orchestration-engine'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { prepareWorkflowExecution, resolveEffectiveExecutionCap, @@ -34,10 +32,6 @@ const inputDataSchema = z }); export const GET = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/execute/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/execute/route.ts index 24b2e973b..3b39681d2 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/execute/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/execute/route.ts @@ -21,8 +21,6 @@ import { prisma } from '@/lib/db/client'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { sseResponse } from '@/lib/api/sse'; import { OrchestrationEngine } from '@/lib/orchestration/engine/orchestration-engine'; import { @@ -36,10 +34,6 @@ import { } from '@/app/api/v1/admin/orchestration/workflows/[id]/_shared/execute-helpers'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/publish/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/publish/route.ts index 8291035fd..f557ca276 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/publish/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/publish/route.ts @@ -15,7 +15,6 @@ import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { publishDraft } from '@/lib/orchestration/workflows/version-service'; import { publishWorkflowSchema } from '@/lib/validations/orchestration'; @@ -23,8 +22,6 @@ import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/rollback/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/rollback/route.ts index dfe196696..cc928fe87 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/rollback/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/rollback/route.ts @@ -15,7 +15,6 @@ import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { rollback } from '@/lib/orchestration/workflows/version-service'; import { rollbackWorkflowSchema } from '@/lib/validations/orchestration'; @@ -23,8 +22,6 @@ import { cuidSchema } from '@/lib/validations/common'; export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/route.ts index 4c5ed3d4d..5e38106b0 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/route.ts @@ -17,7 +17,6 @@ import { successResponse } from '@/lib/api/responses'; import { ConflictError, ForbiddenError, NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction, computeChanges } from '@/lib/orchestration/audit/admin-audit-logger'; import { saveDraft } from '@/lib/orchestration/workflows/version-service'; @@ -33,10 +32,6 @@ function parseWorkflowId(raw: string): string { } export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const id = parseWorkflowId(rawId); @@ -53,8 +48,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; @@ -154,8 +147,6 @@ export const PATCH = withAdminAuth<{ id: string }>(async (request, session, { pa export const DELETE = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.ts index dad0b2b1c..2d978aedf 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.ts @@ -16,7 +16,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { createInitialVersion } from '@/lib/orchestration/workflows/version-service'; @@ -32,8 +31,6 @@ const saveAsTemplateSchema = z.object({ export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const { id: rawId } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.ts index 7a81e0927..50a8ee55f 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.ts @@ -12,7 +12,6 @@ import { prisma } from '@/lib/db/client'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { cuidSchema } from '@/lib/validations/common'; @@ -39,11 +38,7 @@ async function resolveSchedule(rawId: string, rawScheduleId: string) { return schedule; } -export const GET = withAdminAuth(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth(async (_request, _session, { params }) => { const { id, scheduleId } = await params; const schedule = await resolveSchedule(id, scheduleId); @@ -52,8 +47,6 @@ export const GET = withAdminAuth(async (request, _session, { params }) = export const PATCH = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id, scheduleId } = await params; const existing = await resolveSchedule(id, scheduleId); @@ -99,8 +92,6 @@ export const PATCH = withAdminAuth(async (request, session, { params }) export const DELETE = withAdminAuth(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id, scheduleId } = await params; const existing = await resolveSchedule(id, scheduleId); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/schedules/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/schedules/route.ts index ea2d61837..5c2e5774b 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/schedules/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/schedules/route.ts @@ -11,18 +11,13 @@ import { prisma } from '@/lib/db/client'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { successResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { cuidSchema } from '@/lib/validations/common'; import { createScheduleSchema } from '@/lib/validations/orchestration'; import { isValidCron, getNextRunAt } from '@/lib/orchestration/scheduling'; -export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAdminAuth<{ id: string }>(async (_request, _session, { params }) => { const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { @@ -42,8 +37,6 @@ export const GET = withAdminAuth<{ id: string }>(async (request, _session, { par export const POST = withAdminAuth<{ id: string }>(async (request, session, { params }) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/validate/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/validate/route.ts index 73616665e..7e482eb4c 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/validate/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/validate/route.ts @@ -23,17 +23,11 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { validateWorkflow, semanticValidateWorkflow } from '@/lib/orchestration/workflows'; import { cuidSchema } from '@/lib/validations/common'; import { workflowDefinitionSchema } from '@/lib/validations/orchestration'; export const POST = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/workflows/[id]/versions/[version]/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/versions/[version]/route.ts index 3ec6541af..c236b38dc 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/versions/[version]/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/versions/[version]/route.ts @@ -14,8 +14,6 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { getVersion } from '@/lib/orchestration/workflows/version-service'; import { cuidSchema } from '@/lib/validations/common'; @@ -23,10 +21,6 @@ const versionParamSchema = z.coerce.number().int().min(1); export const GET = withAdminAuth<{ id: string; version: string }>( async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId, version: rawVersion } = await params; diff --git a/app/api/v1/admin/orchestration/workflows/[id]/versions/route.ts b/app/api/v1/admin/orchestration/workflows/[id]/versions/route.ts index 76d226bd3..cafff3ca2 100644 --- a/app/api/v1/admin/orchestration/workflows/[id]/versions/route.ts +++ b/app/api/v1/admin/orchestration/workflows/[id]/versions/route.ts @@ -15,8 +15,6 @@ import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateQueryParams } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { prisma } from '@/lib/db/client'; import { listVersions } from '@/lib/orchestration/workflows/version-service'; import { cuidSchema } from '@/lib/validations/common'; @@ -31,10 +29,6 @@ const listVersionsQuerySchema = z.object({ }); export const GET = withAdminAuth<{ id: string }>(async (request, _session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/admin/orchestration/workflows/route.ts b/app/api/v1/admin/orchestration/workflows/route.ts index 0a9d57093..d659faa88 100644 --- a/app/api/v1/admin/orchestration/workflows/route.ts +++ b/app/api/v1/admin/orchestration/workflows/route.ts @@ -18,17 +18,12 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { createWorkflowSchema, listWorkflowsQuerySchema } from '@/lib/validations/orchestration'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { createInitialVersion } from '@/lib/orchestration/workflows/version-service'; export const GET = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); @@ -73,8 +68,6 @@ export const GET = withAdminAuth(async (request, _session) => { export const POST = withAdminAuth(async (request, session) => { const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const log = await getRouteLogger(request); const body = await validateRequestBody(request, createWorkflowSchema); diff --git a/app/api/v1/admin/orchestration/workflows/templates/route.ts b/app/api/v1/admin/orchestration/workflows/templates/route.ts index 079e9b2ed..0671da91a 100644 --- a/app/api/v1/admin/orchestration/workflows/templates/route.ts +++ b/app/api/v1/admin/orchestration/workflows/templates/route.ts @@ -12,14 +12,9 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import type { Prisma } from '@prisma/client'; export const GET = withAdminAuth(async (request) => { - const clientIP = getClientIP(request); - const rateLimit = adminLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); const url = new URL(request.url); const category = url.searchParams.get('category'); const source = url.searchParams.get('source'); diff --git a/eslint.config.mjs b/eslint.config.mjs index 349ff0b77..68b7c65fe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,6 +22,9 @@ export default tseslint.config( // and `tseslint.configs.recommendedTypeChecked` with `projectService: true` // would load every one, exhausting the ESLint heap. '.claude/worktrees/**', + // Throwaway scripts (one-shot codemods, scratch utilities). Gitignored + // but visible to eslint without this exclusion. + '.claude/tmp/**', ], }, diff --git a/tests/integration/api/v1/admin/orchestration/agent-profiles.id.test.ts b/tests/integration/api/v1/admin/orchestration/agent-profiles.id.test.ts index 9d8eadd1e..971824e2d 100644 --- a/tests/integration/api/v1/admin/orchestration/agent-profiles.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agent-profiles.id.test.ts @@ -42,13 +42,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(() => null), diff --git a/tests/integration/api/v1/admin/orchestration/agent-profiles.test.ts b/tests/integration/api/v1/admin/orchestration/agent-profiles.test.ts index ac853ec8d..a3b445925 100644 --- a/tests/integration/api/v1/admin/orchestration/agent-profiles.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agent-profiles.test.ts @@ -41,13 +41,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(() => null), @@ -56,7 +49,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { GET, POST } from '@/app/api/v1/admin/orchestration/agent-profiles/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; const PROFILE_ID = 'cmjbv4i3x00003wsloputgwul'; @@ -185,7 +177,6 @@ describe('GET /api/v1/admin/orchestration/agent-profiles', () => { describe('POST /api/v1/admin/orchestration/agent-profiles', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 401 when unauthenticated', async () => { @@ -200,13 +191,6 @@ describe('POST /api/v1/admin/orchestration/agent-profiles', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await POST(makePostRequest(VALID_PAYLOAD)); - expect(response.status).toBe(429); - }); - it('creates a profile and returns 201 with agentCount: 0', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiAgentProfile.create).mockResolvedValue(makeProfile() as never); diff --git a/tests/integration/api/v1/admin/orchestration/agent-version-restore.test.ts b/tests/integration/api/v1/admin/orchestration/agent-version-restore.test.ts index c434b6f6b..174c3b948 100644 --- a/tests/integration/api/v1/admin/orchestration/agent-version-restore.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agent-version-restore.test.ts @@ -45,13 +45,6 @@ vi.mock('@/lib/db/client', () => { }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/agents.export-import-roundtrip.test.ts b/tests/integration/api/v1/admin/orchestration/agents.export-import-roundtrip.test.ts index 78af4342f..d702daa8b 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.export-import-roundtrip.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.export-import-roundtrip.test.ts @@ -52,13 +52,6 @@ vi.mock('@/lib/db/client', () => { }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/capabilities', () => ({ capabilityDispatcher: { clearCache: vi.fn() }, })); diff --git a/tests/integration/api/v1/admin/orchestration/agents.export.test.ts b/tests/integration/api/v1/admin/orchestration/agents.export.test.ts index 9cb04fe4c..1877bd6e8 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.export.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.export.test.ts @@ -35,18 +35,10 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -110,7 +102,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/agents/export', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -131,29 +122,6 @@ describe('POST /api/v1/admin/orchestration/agents/export', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findMany).mockResolvedValue([ - makeDbAgent(AGENT_ID_1, 'agent-one'), - ] as never); - - await POST(makeRequest({ agentIds: [AGENT_ID_1] })); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest({ agentIds: [AGENT_ID_1] })); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgent.findMany)).not.toHaveBeenCalled(); - }); - }); - describe('Successful export', () => { it('returns bundle with correct shape { success, data: { version, exportedAt, agents } }', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.test.ts index 448ed309c..5127fafb8 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.test.ts @@ -47,13 +47,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/capabilities', () => ({ capabilityDispatcher: { clearCache: vi.fn(), @@ -69,7 +62,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -138,7 +130,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/agents/:id/capabilities', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -159,19 +150,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/capabilities', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue(makeAgent() as never); - vi.mocked(prisma.aiCapability.findUnique).mockResolvedValue(makeCapability() as never); - vi.mocked(prisma.aiAgentCapability.create).mockResolvedValue(makeLink() as never); - - await POST(makeAttachRequest(VALID_ATTACH_BODY), makeAttachParams(AGENT_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('Successful attach', () => { it('creates link and clears capability cache', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -241,7 +219,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/capabilities', () => { describe('PATCH /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -324,21 +301,6 @@ describe('PATCH /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH( - makeCapIdRequest('PATCH', { isEnabled: false }), - makeCapIdParams(AGENT_ID, CAPABILITY_ID) - ); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgentCapability.update)).not.toHaveBeenCalled(); - }); - }); - describe('Error cases', () => { it('returns 404 when link not found (P2025)', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -363,7 +325,6 @@ describe('PATCH /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () describe('DELETE /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -426,21 +387,6 @@ describe('DELETE /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE( - makeCapIdRequest('DELETE'), - makeCapIdParams(AGENT_ID, CAPABILITY_ID) - ); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgentCapability.delete)).not.toHaveBeenCalled(); - }); - }); - describe('Error cases', () => { it('returns 404 when link not found (P2025)', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -465,7 +411,6 @@ describe('DELETE /api/v1/admin/orchestration/agents/:id/capabilities/:capId', () describe('GET /api/v1/admin/orchestration/agents/:id/capabilities', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -554,7 +499,6 @@ describe('${env:VAR} save-time warnings on capability binding routes', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue(makeAgent() as never); vi.mocked(prisma.aiCapability.findUnique).mockResolvedValue(makeCapability() as never); diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts index 67d4a7dc4..7e86984b3 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts @@ -38,20 +38,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -78,7 +70,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/agents/:id/capabilities/usage', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Auth ────────────────────────────────────────────────────────────────── @@ -101,16 +92,6 @@ describe('GET /api/v1/admin/orchestration/agents/:id/capabilities/usage', () => // ── Rate limiting ───────────────────────────────────────────────────────── - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeRequest(), makeParams()); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.$queryRaw)).not.toHaveBeenCalled(); - }); - // ── Validation ──────────────────────────────────────────────────────────── it('returns 400 for invalid agent CUID', async () => { diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts index 79421fe88..b862ccd53 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts @@ -56,13 +56,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ @@ -74,7 +67,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -162,7 +154,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/agents/:id/clone', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Default: transaction resolves with the cloned agent vi.mocked(prisma.$transaction).mockImplementation(async (fn: (tx: never) => unknown) => { const tx = { @@ -195,17 +186,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/clone', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is hit', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeParams(AGENT_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Validation errors', () => { it('returns 400 when id is not a valid CUID', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.instructions-revert.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.instructions-revert.test.ts index 5caf91990..7efb90304 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.instructions-revert.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.instructions-revert.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), @@ -54,7 +47,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -105,7 +97,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/agents/:id/instructions-revert', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -126,18 +117,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/instructions-revert', () = }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue(makeAgentRow() as never); - vi.mocked(prisma.aiAgent.update).mockResolvedValue(makeAgentRow() as never); - - await POST(makeRequest({ versionIndex: 0 }), makeParams(AGENT_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('Critical: revert writes current instructions to history BEFORE overwriting', () => { it('pushes current instructions onto history before writing target version', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.test.ts index 559285fdf..96738d33b 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.test.ts @@ -47,13 +47,6 @@ vi.mock('@/lib/db/client', () => { return { prisma: mock }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), @@ -63,7 +56,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -176,7 +168,6 @@ describe('GET /api/v1/admin/orchestration/agents/:id', () => { describe('PATCH /api/v1/admin/orchestration/agents/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -203,29 +194,6 @@ describe('PATCH /api/v1/admin/orchestration/agents/:id', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on PATCH (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue(makeAgent() as never); - vi.mocked(prisma.aiAgent.update).mockResolvedValue(makeAgent({ name: 'Updated' }) as never); - - await PATCH(makeRequest('PATCH', { name: 'Updated' }), makeParams(AGENT_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makeRequest('PATCH', { name: 'Updated' }), makeParams(AGENT_ID)); - - expect(response.status).toBe(429); - // Prisma was not touched because the guard short-circuits - expect(vi.mocked(prisma.aiAgent.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful update', () => { it('updates non-instructions fields without touching history', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -1120,7 +1088,6 @@ describe('PATCH /api/v1/admin/orchestration/agents/:id', () => { describe('PATCH /api/v1/admin/orchestration/agents/:id — system agent protections', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 403 when trying to deactivate a system agent', async () => { @@ -1207,7 +1174,6 @@ describe('PATCH /api/v1/admin/orchestration/agents/:id — system agent protecti describe('DELETE /api/v1/admin/orchestration/agents/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -1228,18 +1194,6 @@ describe('DELETE /api/v1/admin/orchestration/agents/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeRequest('DELETE'), makeParams(AGENT_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgent.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful soft delete', () => { it('sets isActive to false and returns success', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/agents.import.test.ts b/tests/integration/api/v1/admin/orchestration/agents.import.test.ts index ae2fee778..2d3c114a2 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.import.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.import.test.ts @@ -57,13 +57,6 @@ vi.mock('@/lib/db/client', () => { }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/capabilities', () => ({ capabilityDispatcher: { clearCache: vi.fn() }, })); @@ -77,7 +70,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -171,7 +163,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/agents/import', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Default: no existing agents (clean import), no capabilities const tx = getTxMock(); @@ -206,16 +197,6 @@ describe('POST /api/v1/admin/orchestration/agents/import', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - - await POST(makeRequest({ bundle: makeBundle([makeBundledAgent('new-agent')]) })); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('Clean import (new agents)', () => { it('creates agents and returns imported count', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/agents.invite-tokens.test.ts b/tests/integration/api/v1/admin/orchestration/agents.invite-tokens.test.ts index 5a42d1167..8018975ad 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.invite-tokens.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.invite-tokens.test.ts @@ -55,13 +55,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -75,7 +68,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -145,7 +137,6 @@ describe('GET /api/v1/admin/orchestration/agents/:id/invite-tokens', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -301,7 +292,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/invite-tokens', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -326,26 +316,6 @@ describe('POST /api/v1/admin/orchestration/agents/:id/invite-tokens', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(prisma.aiAgent.findFirst).mockResolvedValue(makeInviteOnlyAgent() as never); - vi.mocked(prisma.aiAgentInviteToken.create).mockResolvedValue(makeToken() as never); - - await POST(makePostRequest({ label: 'Test' }), makeAgentParams()); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 and does not write when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest({ label: 'Test' }), makeAgentParams()); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgentInviteToken.create)).not.toHaveBeenCalled(); - }); - }); - describe('Successful creation', () => { it('creates token with all optional fields and returns 201', async () => { vi.mocked(prisma.aiAgent.findFirst).mockResolvedValue(makeInviteOnlyAgent() as never); @@ -570,7 +540,6 @@ describe('DELETE /api/v1/admin/orchestration/agents/:id/invite-tokens/:tokenId', beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -595,28 +564,6 @@ describe('DELETE /api/v1/admin/orchestration/agents/:id/invite-tokens/:tokenId', }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on DELETE', async () => { - vi.mocked(prisma.aiAgentInviteToken.findFirst).mockResolvedValue( - makeToken({ revokedAt: null }) as never - ); - vi.mocked(prisma.aiAgentInviteToken.update).mockResolvedValue({} as never); - - await DELETE(makeDeleteRequest(), makeTokenParams()); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 and does not write when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeTokenParams()); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiAgentInviteToken.update)).not.toHaveBeenCalled(); - }); - }); - describe('Successful revoke', () => { it('revokes an active token and returns success message', async () => { vi.mocked(prisma.aiAgentInviteToken.findFirst).mockResolvedValue( @@ -739,7 +686,6 @@ describe('CRUD flow: create → list → revoke', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('token created via POST appears in GET list and can be revoked via DELETE', async () => { diff --git a/tests/integration/api/v1/admin/orchestration/agents.test.ts b/tests/integration/api/v1/admin/orchestration/agents.test.ts index f4d4d82c7..37cd8c83c 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.test.ts @@ -47,13 +47,6 @@ vi.mock('@/lib/orchestration/llm/cost-tracker', () => ({ getMonthToDateGlobalSpend: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), @@ -63,7 +56,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -308,7 +300,6 @@ describe('GET /api/v1/admin/orchestration/agents', () => { describe('POST /api/v1/admin/orchestration/agents', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -329,26 +320,6 @@ describe('POST /api/v1/admin/orchestration/agents', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.create).mockResolvedValue(makeAgent() as never); - - await POST(makePostRequest(VALID_AGENT)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_AGENT)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful creation', () => { it('creates agent and returns 201', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/approvals.history.test.ts b/tests/integration/api/v1/admin/orchestration/approvals.history.test.ts index afdb38a6a..b09abce69 100644 --- a/tests/integration/api/v1/admin/orchestration/approvals.history.test.ts +++ b/tests/integration/api/v1/admin/orchestration/approvals.history.test.ts @@ -32,13 +32,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // Stub the route logger so log.info calls don't pollute test output. @@ -53,7 +46,6 @@ vi.mock('@/lib/api/context', () => ({ import { GET } from '@/app/api/v1/admin/orchestration/approvals/history/route'; import { prisma } from '@/lib/db/client'; import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ───────────────────────────────────────────────────────────────── @@ -252,17 +244,6 @@ describe('GET /api/v1/admin/orchestration/approvals/history', () => { expect(res.status).toBe(403); }); - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const res = await GET(makeRequest()); - expect(res.status).toBe(429); - }); - it('returns 400 for invalid query params', async () => { // `limit` must be a positive integer per approvalHistoryQuerySchema. const res = await GET(makeRequest('?limit=not-a-number')); diff --git a/tests/integration/api/v1/admin/orchestration/backup/export.test.ts b/tests/integration/api/v1/admin/orchestration/backup/export.test.ts index 83d786e9d..9d0057bb4 100644 --- a/tests/integration/api/v1/admin/orchestration/backup/export.test.ts +++ b/tests/integration/api/v1/admin/orchestration/backup/export.test.ts @@ -25,13 +25,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -54,7 +47,6 @@ vi.mock('@/lib/api/context', () => ({ // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { exportOrchestrationConfig } from '@/lib/orchestration/backup/exporter'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -91,7 +83,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/backup/export', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(exportOrchestrationConfig).mockResolvedValue(makeBackupPayload() as never); }); @@ -113,18 +104,6 @@ describe('POST /api/v1/admin/orchestration/backup/export', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest()); - - expect(response.status).toBe(429); - expect(vi.mocked(exportOrchestrationConfig)).not.toHaveBeenCalled(); - }); - }); - describe('Successful export', () => { it('returns 200 with JSON body', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/backup/import.test.ts b/tests/integration/api/v1/admin/orchestration/backup/import.test.ts index d17fc19fa..1857fd2e9 100644 --- a/tests/integration/api/v1/admin/orchestration/backup/import.test.ts +++ b/tests/integration/api/v1/admin/orchestration/backup/import.test.ts @@ -26,13 +26,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -55,7 +48,6 @@ vi.mock('@/lib/api/context', () => ({ // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { importOrchestrationConfig } from '@/lib/orchestration/backup/importer'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; @@ -114,7 +106,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/backup/import', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(importOrchestrationConfig).mockResolvedValue(makeImportResult()); }); @@ -136,17 +127,6 @@ describe('POST /api/v1/admin/orchestration/backup/import', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(makeValidPayload())); - - expect(response.status).toBe(429); - }); - }); - describe('Validation errors', () => { it('returns 400 VALIDATION_ERROR when request body is not valid JSON', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/capabilities.id.agents.test.ts b/tests/integration/api/v1/admin/orchestration/capabilities.id.agents.test.ts index 2c34be737..3db331bd9 100644 --- a/tests/integration/api/v1/admin/orchestration/capabilities.id.agents.test.ts +++ b/tests/integration/api/v1/admin/orchestration/capabilities.id.agents.test.ts @@ -34,18 +34,10 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -98,7 +90,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/capabilities/:id/agents', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Authentication & Authorization ───────────────────────────────────────── @@ -242,15 +233,4 @@ describe('GET /api/v1/admin/orchestration/capabilities/:id/agents', () => { }); // ── Rate limiting ────────────────────────────────────────────────────────── - - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeRequest(CAPABILITY_ID), makeParams(CAPABILITY_ID)); - - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/capabilities.test.ts b/tests/integration/api/v1/admin/orchestration/capabilities.test.ts index 5498e8186..ccc3a6112 100644 --- a/tests/integration/api/v1/admin/orchestration/capabilities.test.ts +++ b/tests/integration/api/v1/admin/orchestration/capabilities.test.ts @@ -50,13 +50,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/capabilities', () => ({ capabilityDispatcher: { clearCache: vi.fn() }, })); @@ -70,7 +63,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { capabilityDispatcher } from '@/lib/orchestration/capabilities'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -230,7 +222,6 @@ describe('GET /api/v1/admin/orchestration/capabilities', () => { describe('POST /api/v1/admin/orchestration/capabilities', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -251,17 +242,6 @@ describe('POST /api/v1/admin/orchestration/capabilities', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiCapability.create).mockResolvedValue(makeCapability() as never); - - await POST(makeBodyRequest('POST', VALID_CAPABILITY)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('Successful creation', () => { it('creates capability and returns 201', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -420,7 +400,6 @@ describe('GET /api/v1/admin/orchestration/capabilities/:id', () => { describe('PATCH /api/v1/admin/orchestration/capabilities/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -447,21 +426,6 @@ describe('PATCH /api/v1/admin/orchestration/capabilities/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH( - makeByIdRequest('PATCH', { name: 'Updated' }), - makeParams(CAPABILITY_ID) - ); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiCapability.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful update', () => { it('updates capability and clears cache', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -606,7 +570,6 @@ describe('PATCH /api/v1/admin/orchestration/capabilities/:id', () => { describe('DELETE /api/v1/admin/orchestration/capabilities/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -627,18 +590,6 @@ describe('DELETE /api/v1/admin/orchestration/capabilities/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeByIdRequest('DELETE'), makeParams(CAPABILITY_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiCapability.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful soft delete', () => { it('sets isActive to false, calls clearCache, and returns success', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts b/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts index fcabe52ff..9f903e7d1 100644 --- a/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts +++ b/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts @@ -60,8 +60,6 @@ vi.mock('@/lib/db/client', () => ({ import { auth } from '@/lib/auth/config'; import { streamChat } from '@/lib/orchestration/chat'; -import { adminLimiter, agentChatLimiter } from '@/lib/security/rate-limit'; -import { prisma } from '@/lib/db/client'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -104,7 +102,6 @@ const VALID_BODY = { describe('POST /api/v1/admin/orchestration/chat/stream', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -296,71 +293,4 @@ describe('POST /api/v1/admin/orchestration/chat/stream', () => { expect(body).not.toContain('internal details here'); }); }); - - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(streamChat).mockReturnValue(makeStreamEvents([{ type: 'done' }]) as never); - - await POST(makePostRequest(VALID_BODY)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_BODY)); - - expect(response.status).toBe(429); - // streamChat was never called because the guard short-circuits - expect(vi.mocked(streamChat)).not.toHaveBeenCalled(); - }); - - it('applies per-agent rateLimitRpm via agentChatLimiter', async () => { - // Admins are subject to the same per-agent throttle the consumer - // route uses. Setting `rateLimitRpm: 1` on the pattern-advisor - // (or any other admin-facing agent) must actually throttle the - // admin's chat — previously this field was only enforced on the - // consumer endpoint. - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue({ - id: 'agent-123', - rateLimitRpm: 1, - } as never); - vi.mocked(streamChat).mockReturnValue(makeStreamEvents([{ type: 'done' }]) as never); - - await POST(makePostRequest({ ...VALID_BODY, agentSlug: 'pattern-advisor' })); - - expect(vi.mocked(agentChatLimiter.check)).toHaveBeenCalledWith( - expect.stringContaining('agent-123'), - 1 - ); - }); - - it('returns 429 when the per-agent limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue({ - id: 'agent-123', - rateLimitRpm: 1, - } as never); - vi.mocked(agentChatLimiter.check).mockReturnValueOnce({ success: false } as never); - - const response = await POST(makePostRequest(VALID_BODY)); - - expect(response.status).toBe(429); - expect(vi.mocked(streamChat)).not.toHaveBeenCalled(); - }); - - it('returns 404 when the agent slug does not exist', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiAgent.findUnique).mockResolvedValueOnce(null); - - const response = await POST(makePostRequest({ ...VALID_BODY, agentSlug: 'missing' })); - - expect(response.status).toBe(404); - expect(vi.mocked(streamChat)).not.toHaveBeenCalled(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts index ef58cacb2..e3797dee3 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts @@ -51,20 +51,12 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -93,7 +85,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/conversations/clear', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -218,17 +209,6 @@ describe('POST /api/v1/admin/orchestration/conversations/clear', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiConversation.deleteMany).mockResolvedValue({ count: 0 }); - - await POST(makePostRequest({ agentId: AGENT_ID })); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('Cross-user clear (targeted userId)', () => { it('scopes WHERE to the supplied userId and audit-logs the bulk clear', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts index 75dc1f736..16c146bc9 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts @@ -52,13 +52,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logConversationAccess: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/conversations.id.patch.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.id.patch.test.ts index ef8d82295..1d1a9b2bc 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.id.patch.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.id.patch.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts index e1afe5a06..91c8c2c41 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts @@ -45,20 +45,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -104,7 +96,6 @@ async function parseJson(response: Response): Promise { describe('DELETE /api/v1/admin/orchestration/conversations/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -192,16 +183,4 @@ describe('DELETE /api/v1/admin/orchestration/conversations/:id', () => { expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('calls adminLimiter.check on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiConversation.findFirst).mockResolvedValue(makeConversation() as never); - vi.mocked(prisma.aiConversation.delete).mockResolvedValue(makeConversation() as never); - - await DELETE(makeRequest(), makeParams(CONV_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/discovery.models.test.ts b/tests/integration/api/v1/admin/orchestration/discovery.models.test.ts index 34e424ebe..cdd194f35 100644 --- a/tests/integration/api/v1/admin/orchestration/discovery.models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/discovery.models.test.ts @@ -55,19 +55,11 @@ vi.mock('@/lib/orchestration/llm/model-registry', () => ({ getModelsByProvider: vi.fn(() => []), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - import { GET } from '@/app/api/v1/admin/orchestration/discovery/models/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { isApiKeyEnvVarSet } from '@/lib/orchestration/llm/provider-manager'; import { getModelsByProvider, refreshFromOpenRouter } from '@/lib/orchestration/llm/model-registry'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -122,7 +114,6 @@ describe('GET /api/v1/admin/orchestration/discovery/models', () => { vi.mocked(isApiKeyEnvVarSet).mockReturnValue(true); vi.mocked(refreshFromOpenRouter).mockResolvedValue(); vi.mocked(getModelsByProvider).mockReturnValue([]); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication', () => { @@ -139,15 +130,6 @@ describe('GET /api/v1/admin/orchestration/discovery/models', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await GET(makeRequest()); - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { it('returns 400 when providerSlug is missing', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts b/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts index 9f0ea1c1c..a76974713 100644 --- a/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts @@ -42,13 +42,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // --------------------------------------------------------------------------- @@ -56,7 +49,6 @@ vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // --------------------------------------------------------------------------- import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; // --------------------------------------------------------------------------- // Helpers @@ -84,7 +76,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/embedding-models', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ─── Authentication & Authorization ─────────────────────────────────────── @@ -126,31 +117,6 @@ describe('GET /api/v1/admin/orchestration/embedding-models', () => { // ─── Rate Limiting ──────────────────────────────────────────────────────── - describe('Rate Limiting', () => { - it('should call adminLimiter.check on every request', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - - // Act - await GET(makeGetRequest()); - - // Assert - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('should return 429 when the rate limit is exceeded', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const response = await GET(makeGetRequest()); - - // Assert - expect(response.status).toBe(429); - }); - }); - // ─── Response Format ────────────────────────────────────────────────────── describe('Response format', () => { diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts index 0dff20d39..1088134c9 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts @@ -53,20 +53,12 @@ vi.mock('@/lib/orchestration/evaluations', () => ({ completeEvaluationSession: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { completeEvaluationSession } from '@/lib/orchestration/evaluations'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -124,7 +116,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/evaluations/:id/complete', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -270,17 +261,6 @@ describe('POST /api/v1/admin/orchestration/evaluations/:id/complete', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is hit', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeParams(SESSION_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Validation errors', () => { it('returns 400 when id is not a valid CUID', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.id.rescore.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.id.rescore.test.ts index 109915257..05945ce1c 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.id.rescore.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.id.rescore.test.ts @@ -45,18 +45,10 @@ vi.mock('@/lib/orchestration/evaluations', () => ({ rescoreEvaluationSession: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); import { auth } from '@/lib/auth/config'; import { rescoreEvaluationSession } from '@/lib/orchestration/evaluations'; -import { adminLimiter } from '@/lib/security/rate-limit'; const SESSION_ID = 'cmjbv4i3x00003wsloputgwu3'; const INVALID_ID = 'not-a-cuid'; @@ -103,7 +95,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/evaluations/:id/rescore', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.id.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.id.test.ts index 326cc5e70..47d79093d 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.id.test.ts @@ -44,20 +44,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -115,7 +107,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/evaluations/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -179,7 +170,6 @@ describe('GET /api/v1/admin/orchestration/evaluations/:id', () => { describe('PATCH /api/v1/admin/orchestration/evaluations/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -295,15 +285,4 @@ describe('PATCH /api/v1/admin/orchestration/evaluations/:id', () => { expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('returns 429 when rate limit is hit on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ title: 'Updated' }), makeParams(SESSION_ID)); - - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.test.ts index d7178fcf7..9eb77cc25 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.test.ts @@ -48,20 +48,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -112,7 +104,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/evaluations', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -236,7 +227,6 @@ describe('GET /api/v1/admin/orchestration/evaluations', () => { describe('POST /api/v1/admin/orchestration/evaluations', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -317,15 +307,4 @@ describe('POST /api/v1/admin/orchestration/evaluations', () => { expect(data.error.code).toBe('NOT_FOUND'); }); }); - - describe('Rate limiting', () => { - it('returns 429 when rate limit is hit', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest({ agentId: AGENT_ID, title: 'My Eval' })); - - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/executions.id.approve.test.ts b/tests/integration/api/v1/admin/orchestration/executions.id.approve.test.ts index 05098a046..de214d6e3 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.id.approve.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.id.approve.test.ts @@ -44,13 +44,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/scheduling', () => ({ resumeApprovedExecution: vi.fn(() => Promise.resolve()), })); @@ -68,7 +61,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { resumeApprovedExecution } from '@/lib/orchestration/scheduling'; import { logger } from '@/lib/logging'; @@ -134,7 +126,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/executions/:id/approve', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 1 } as never); }); @@ -150,13 +141,6 @@ describe('POST /api/v1/admin/orchestration/executions/:id/approve', () => { expect(response.status).toBe(403); }); - it('returns 429 when adminLimiter blocks the request', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await POST(makePostRequest(), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid CUID param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); const response = await POST(makePostRequest(), makeParams(INVALID_ID)); diff --git a/tests/integration/api/v1/admin/orchestration/executions.id.cancel.test.ts b/tests/integration/api/v1/admin/orchestration/executions.id.cancel.test.ts index 71e58e1ce..11cd0ab9b 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.id.cancel.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.id.cancel.test.ts @@ -51,18 +51,10 @@ vi.mock('@/lib/db/client', () => { return { prisma }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -115,7 +107,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/executions/:id/cancel', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 1 } as never); }); @@ -133,15 +124,6 @@ describe('POST /api/v1/admin/orchestration/executions/:id/cancel', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await POST(makePostRequest(), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - }); - describe('CUID validation', () => { it('returns 400 for an invalid (non-CUID) id param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/executions.id.live.test.ts b/tests/integration/api/v1/admin/orchestration/executions.id.live.test.ts index a51c35fa6..9667263a5 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.id.live.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.id.live.test.ts @@ -33,20 +33,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; const EXECUTION_ID = 'cmjbv4i3x00003wsloputgwul'; const ADMIN_ID = 'cmjbv4i3x00003wsloputgwul'; @@ -117,7 +109,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/executions/:id/live', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiCostLog.findMany).mockResolvedValue([] as never); vi.mocked(prisma.aiWorkflowRunningStep.findMany).mockResolvedValue([] as never); }); @@ -140,18 +131,6 @@ describe('GET /api/v1/admin/orchestration/executions/:id/live', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const response = await GET(makeRequest(), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - it('returns 404 when execution not found', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiWorkflowExecution.findUnique).mockResolvedValue(null); diff --git a/tests/integration/api/v1/admin/orchestration/executions.id.reject.test.ts b/tests/integration/api/v1/admin/orchestration/executions.id.reject.test.ts index c2f1c62bc..b1e6eaa77 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.id.reject.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.id.reject.test.ts @@ -44,18 +44,10 @@ vi.mock('@/lib/db/client', () => { return { prisma }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -118,7 +110,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/executions/:id/reject', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 1 } as never); }); @@ -134,13 +125,6 @@ describe('POST /api/v1/admin/orchestration/executions/:id/reject', () => { expect(response.status).toBe(403); }); - it('returns 429 when adminLimiter blocks the request', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await POST(makePostRequest({ reason: 'No' }), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid CUID param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); const response = await POST(makePostRequest({ reason: 'No' }), makeParams(INVALID_ID)); diff --git a/tests/integration/api/v1/admin/orchestration/executions.id.status.test.ts b/tests/integration/api/v1/admin/orchestration/executions.id.status.test.ts index b8ac0bb8c..dac5c8f0e 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.id.status.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.id.status.test.ts @@ -34,13 +34,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -49,7 +42,6 @@ vi.mock('@/lib/security/ip', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -91,7 +83,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/executions/:id/status', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 401 when unauthenticated', async () => { @@ -112,19 +103,6 @@ describe('GET /api/v1/admin/orchestration/executions/:id/status', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const response = await GET(makeRequest(), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - it('returns 404 when execution not found', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiWorkflowExecution.findUnique).mockResolvedValue(null); diff --git a/tests/integration/api/v1/admin/orchestration/executions.list.test.ts b/tests/integration/api/v1/admin/orchestration/executions.list.test.ts index 45f3488c1..4f34f15e2 100644 --- a/tests/integration/api/v1/admin/orchestration/executions.list.test.ts +++ b/tests/integration/api/v1/admin/orchestration/executions.list.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/experiments.id.run.test.ts b/tests/integration/api/v1/admin/orchestration/experiments.id.run.test.ts index 5ebf715b2..819655cb4 100644 --- a/tests/integration/api/v1/admin/orchestration/experiments.id.run.test.ts +++ b/tests/integration/api/v1/admin/orchestration/experiments.id.run.test.ts @@ -52,13 +52,6 @@ vi.mock('@/lib/db/client', () => { }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -78,7 +71,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -135,7 +127,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/experiments/:id/run', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Outer findUnique: 404 check (select: { id: true }) vi.mocked(prisma.aiExperiment.findUnique).mockResolvedValue({ id: EXPERIMENT_ID } as never); // Inner tx findUnique: full experiment with variants @@ -161,15 +152,6 @@ describe('POST /api/v1/admin/orchestration/experiments/:id/run', () => { expect(response.status).toBe(403); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeContext()); - - expect(response.status).toBe(429); - }); }); describe('Not found', () => { diff --git a/tests/integration/api/v1/admin/orchestration/experiments.id.test.ts b/tests/integration/api/v1/admin/orchestration/experiments.id.test.ts index f4b8aff7c..0a2f666a1 100644 --- a/tests/integration/api/v1/admin/orchestration/experiments.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/experiments.id.test.ts @@ -37,13 +37,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -63,7 +56,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -173,7 +165,6 @@ describe('GET /api/v1/admin/orchestration/experiments/:id', () => { describe('PATCH /api/v1/admin/orchestration/experiments/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiExperiment.findUnique).mockResolvedValue(makeExperiment() as never); vi.mocked(prisma.aiExperiment.update).mockResolvedValue(makeExperiment() as never); }); @@ -194,15 +185,6 @@ describe('PATCH /api/v1/admin/orchestration/experiments/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ name: 'New Name' }), makeContext()); - - expect(response.status).toBe(429); - }); - it('returns 400 when no fields are provided', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -314,7 +296,6 @@ describe('PATCH /api/v1/admin/orchestration/experiments/:id', () => { describe('DELETE /api/v1/admin/orchestration/experiments/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiExperiment.findUnique).mockResolvedValue(makeExperiment() as never); vi.mocked(prisma.aiExperiment.delete).mockResolvedValue(makeExperiment() as never); }); @@ -327,15 +308,6 @@ describe('DELETE /api/v1/admin/orchestration/experiments/:id', () => { expect(response.status).toBe(401); }); - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeContext()); - - expect(response.status).toBe(429); - }); - it('returns 404 when experiment not found', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiExperiment.findUnique).mockResolvedValue(null); diff --git a/tests/integration/api/v1/admin/orchestration/experiments.test.ts b/tests/integration/api/v1/admin/orchestration/experiments.test.ts index c9b0b8e5a..81282114b 100644 --- a/tests/integration/api/v1/admin/orchestration/experiments.test.ts +++ b/tests/integration/api/v1/admin/orchestration/experiments.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -62,7 +55,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -186,7 +178,6 @@ describe('GET /api/v1/admin/orchestration/experiments', () => { describe('POST /api/v1/admin/orchestration/experiments', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -207,17 +198,6 @@ describe('POST /api/v1/admin/orchestration/experiments', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_BODY)); - - expect(response.status).toBe(429); - }); - }); - describe('Validation errors', () => { it('returns 400 when fewer than 2 variants are provided', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge-document-tags.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge-document-tags.test.ts index eada2fe1b..12ee838eb 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge-document-tags.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge-document-tags.test.ts @@ -37,13 +37,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts index 83ba275b5..703d67296 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts @@ -55,13 +55,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ computeChanges: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); import { auth } from '@/lib/auth/config'; @@ -71,7 +64,6 @@ import { NoChunksToEnrichError, } from '@/lib/orchestration/knowledge/keyword-enricher'; import { NoDefaultModelConfiguredError } from '@/lib/orchestration/llm/settings-resolver'; -import { adminLimiter } from '@/lib/security/rate-limit'; const DOC_ID = 'cmjbv4i3x00003wsloputgwul'; const INVALID_ID = 'not-a-cuid'; @@ -113,7 +105,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/enrich-keywords', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -225,24 +216,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/enrich-keywor }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiKnowledgeDocument.findUnique).mockResolvedValue(makeDocument() as never); - vi.mocked(enrichDocumentKeywords).mockResolvedValue({ - chunksProcessed: 1, - chunksSkipped: 0, - chunksFailed: 0, - tokensUsed: 10, - costUsd: 0, - model: 'local', - }); - - await POST(makeRequest(), makeParams(DOC_ID)); - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); - describe('NoChunksToEnrichError handling', () => { it('returns 409 when enrichDocumentKeywords throws NoChunksToEnrichError', async () => { // This branch covers when the enricher itself detects no chunks at runtime diff --git a/tests/integration/api/v1/admin/orchestration/knowledge-tags.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge-tags.test.ts index 772ddcc55..c5cd71e6f 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge-tags.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge-tags.test.ts @@ -49,13 +49,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.bulk.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.bulk.test.ts index 1c8c90aa0..f9a6f47ee 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.bulk.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.bulk.test.ts @@ -37,13 +37,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -87,7 +80,6 @@ vi.mock('@/lib/logging', () => ({ // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { uploadDocument, uploadDocumentFromBuffer, @@ -136,7 +128,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/documents/bulk', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(uploadDocument).mockResolvedValue(makeDocument() as never); vi.mocked(uploadDocumentFromBuffer).mockResolvedValue(makeDocument() as never); }); @@ -165,20 +156,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/bulk', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const fd = makeFormDataWithFiles([ - new File(['content'], 'doc.md', { type: 'text/markdown' }), - ]); - - const response = await POST(makePostRequest(fd)); - - expect(response.status).toBe(429); - }); - }); - describe('Batch validation', () => { it('returns 400 when no files are provided', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.fetch-url.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.fetch-url.test.ts index 0512ed822..b2e307183 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.fetch-url.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.fetch-url.test.ts @@ -34,13 +34,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -84,7 +77,6 @@ vi.mock('@/lib/api/context', () => ({ // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { fetchDocumentFromUrl } from '@/lib/orchestration/knowledge/url-fetcher'; import { uploadDocument, @@ -137,7 +129,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/documents/fetch-url', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(fetchDocumentFromUrl).mockResolvedValue(makeFetchedDocument() as never); vi.mocked(uploadDocument).mockResolvedValue(makeDocument() as never); vi.mocked(uploadDocumentFromBuffer).mockResolvedValue(makeDocument() as never); @@ -161,17 +152,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/fetch-url', () => }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest({ url: VALID_URL })); - - expect(response.status).toBe(429); - }); - }); - describe('Validation errors', () => { it('returns 400 when url field is missing', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.chunks.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.chunks.test.ts index d761ca579..1888e63a5 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.chunks.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.chunks.test.ts @@ -38,20 +38,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────��─────────────────────��─────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ────��─────────────────────────────────────────────────────────── @@ -100,7 +92,6 @@ function makeParams(id: string) { describe('GET /api/v1/admin/orchestration/knowledge/documents/:id/chunks', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -163,19 +154,4 @@ describe('GET /api/v1/admin/orchestration/knowledge/documents/:id/chunks', () => expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const response = await GET(makeRequest(DOC_ID), makeParams(DOC_ID)); - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.confirm.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.confirm.test.ts index 004768ea5..3b179f35e 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.confirm.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.confirm.test.ts @@ -41,20 +41,12 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ computeChanges: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { confirmPreview } from '@/lib/orchestration/knowledge/document-manager'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -106,7 +98,6 @@ function makeParams(id: string) { describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/confirm', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -185,19 +176,4 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/confirm', () expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const response = await POST(makeRequest(DOC_ID, { documentId: DOC_ID }), makeParams(DOC_ID)); - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts index b0ad8614c..61739a0b1 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts @@ -49,13 +49,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ computeChanges: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── @@ -63,7 +56,6 @@ vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { rechunkDocument } from '@/lib/orchestration/knowledge/document-manager'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -111,7 +103,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/rechunk', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -208,18 +199,4 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/rechunk', () expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiKnowledgeDocument.findUnique).mockResolvedValue( - makeDocument({ status: 'ready' }) as never - ); - vi.mocked(rechunkDocument).mockResolvedValue(makeDocument() as never); - - await POST(makeRequest(), makeParams(DOC_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts index ad3832777..3c58c5f6e 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts @@ -61,13 +61,6 @@ vi.mock('@/lib/orchestration/knowledge/document-manager', () => ({ rechunkDocument: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -90,7 +83,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { rechunkDocument } from '@/lib/orchestration/knowledge/document-manager'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -139,7 +131,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/retry', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -160,17 +151,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/retry', () => }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest(), makeParams(DOC_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful retry', () => { it('returns 200 with the reprocessed document', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts index b64bfde11..8adee4932 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts @@ -47,13 +47,6 @@ vi.mock('@/lib/orchestration/knowledge/document-manager', () => ({ deleteDocument: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ @@ -66,7 +59,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { deleteDocument } from '@/lib/orchestration/knowledge/document-manager'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -180,7 +172,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/documents/:id', () => { describe('DELETE /api/v1/admin/orchestration/knowledge/documents/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -246,16 +237,4 @@ describe('DELETE /api/v1/admin/orchestration/knowledge/documents/:id', () => { expect(response.status).toBe(400); }); }); - - describe('Rate limiting', () => { - it('calls adminLimiter.check on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiKnowledgeDocument.findUnique).mockResolvedValue(makeDocument() as never); - vi.mocked(deleteDocument).mockResolvedValue(undefined); - - await DELETE(makeRequest('DELETE'), makeParams(DOC_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts index 290c8e263..b794572ae 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts @@ -58,13 +58,6 @@ vi.mock('@/lib/orchestration/knowledge/document-manager', () => ({ previewDocument: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── @@ -76,7 +69,6 @@ import { uploadDocument, uploadDocumentFromBuffer, } from '@/lib/orchestration/knowledge/document-manager'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -206,7 +198,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/documents', () => { describe('POST /api/v1/admin/orchestration/knowledge/documents', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -447,33 +438,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(uploadDocument).mockResolvedValue(makeDocument() as never); - const formData = new FormData(); - formData.append('file', new File(['# Hello'], 'hello.md', { type: 'text/markdown' })); - - await POST(makePostRequestWithFormData(formData)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when adminLimiter rejects the POST', async () => { - // Verifies the 429 response shape end-to-end. The unit-level test - // covers the same case (route.test.ts) — this integration variant - // pins the contract through the wider auth + handler wiring. - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const formData = new FormData(); - formData.append('file', new File(['# Hello'], 'hello.md', { type: 'text/markdown' })); - - const response = await POST(makePostRequestWithFormData(formData)); - - expect(response.status).toBe(429); - }); - }); - describe('Pre-parse body-size guard', () => { function makePostRequestWithContentLength( contentLength: string | null, @@ -530,13 +494,5 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents', () => { expect(response.status).toBe(201); }); - - it('still consumes the rate limit budget on oversized rejections', async () => { - // Auth + rate-limit run before the body cap so an authenticated - // attacker still pays for their oversize attempts. - await POST(makePostRequestWithContentLength('1073741824')); - - expect(adminLimiter.check).toHaveBeenCalled(); - }); }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts index 31a8e0ceb..99bf4f195 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ computeChanges: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -68,7 +61,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { embedChunks } from '@/lib/orchestration/knowledge/seeder'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -89,7 +81,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/embed', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -110,18 +101,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/embed', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest()); - - expect(response.status).toBe(429); - expect(vi.mocked(embedChunks)).not.toHaveBeenCalled(); - }); - }); - describe('Successful embedding', () => { it('returns 200 with embedChunks result and calls embedChunks once', async () => { const embedResult = { processed: 5, skipped: 0, errors: 0 }; diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts index c320e4b76..7d7347b3e 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts @@ -45,13 +45,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -73,7 +66,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -103,7 +95,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/embedding-status', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Default: no env key delete process.env['OPENAI_API_KEY']; }); @@ -135,17 +126,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/embedding-status', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(429); - }); - }); - describe('Successful status response', () => { it('returns correct counts and hasActiveProvider: true when provider row exists', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts index 04b21e483..640f639ff 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts @@ -45,20 +45,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -128,7 +120,6 @@ interface GraphResponse { describe('GET /api/v1/admin/orchestration/knowledge/graph', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Auth ────────────────────────────────────────────────────────────────── @@ -151,16 +142,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/graph', () => { // ── Rate limiting ───────────────────────────────────────────────────────── - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiKnowledgeDocument.findMany)).not.toHaveBeenCalled(); - }); - // ── Validation ──────────────────────────────────────────────────────────── it('returns 400 for invalid scope value', async () => { diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.patterns.number.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.patterns.number.test.ts index 05e07295b..cf7e35ca6 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.patterns.number.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.patterns.number.test.ts @@ -34,13 +34,6 @@ vi.mock('@/lib/orchestration/knowledge/search', () => ({ getPatternDetail: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.patterns.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.patterns.test.ts index 9dd6c1de9..e73017c5c 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.patterns.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.patterns.test.ts @@ -25,20 +25,12 @@ vi.mock('@/lib/orchestration/knowledge/search', () => ({ listPatterns: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ──────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { listPatterns } from '@/lib/orchestration/knowledge/search'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -113,17 +105,4 @@ describe('GET /api/v1/admin/orchestration/knowledge/patterns', () => { const res = await GET(makeRequest()); expect(res.status).toBe(403); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const res = await GET(makeRequest()); - expect(res.status).toBe(429); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts index bfabde856..753cd13f9 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/orchestration/knowledge/resolveAgentDocumentAccess', () => ({ resolveAgentDocumentAccess: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── @@ -52,7 +45,6 @@ vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); import { auth } from '@/lib/auth/config'; import { searchKnowledge } from '@/lib/orchestration/knowledge/search'; import { resolveAgentDocumentAccess } from '@/lib/orchestration/knowledge/resolveAgentDocumentAccess'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -89,7 +81,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/search', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -186,27 +177,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/search', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(searchKnowledge).mockResolvedValue([]); - - await POST(makePostRequest({ query: 'agent patterns' })); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest({ query: 'agent patterns' })); - - expect(response.status).toBe(429); - expect(vi.mocked(searchKnowledge)).not.toHaveBeenCalled(); - }); - }); - describe('Agent-scoped search (agentId param)', () => { const AGENT_ID = 'cmjbv4i3x00003wsloputgwul'; diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts index a8e264c62..f9f4f38a4 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts @@ -40,20 +40,12 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ computeChanges: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { seedChunks } from '@/lib/orchestration/knowledge/seeder'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -74,7 +66,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/knowledge/seed', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -127,25 +118,4 @@ describe('POST /api/v1/admin/orchestration/knowledge/seed', () => { expect(calledWith).toMatch(/prisma[/\\]seeds[/\\]data[/\\]chunks[/\\]chunks\.json$/); }); }); - - describe('Rate limiting', () => { - it('calls adminLimiter.check on POST', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(seedChunks).mockResolvedValue(undefined); - - await POST(makeRequest()); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest()); - - expect(response.status).toBe(429); - expect(vi.mocked(seedChunks)).not.toHaveBeenCalled(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.tags.id.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.tags.id.test.ts index 0bd8aec2c..024166989 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.tags.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.tags.id.test.ts @@ -47,13 +47,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ @@ -75,7 +68,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { invalidateAllAgentAccess } from '@/lib/orchestration/knowledge/resolveAgentDocumentAccess'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -122,7 +114,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/knowledge/tags/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -249,7 +240,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/tags/:id', () => { describe('PATCH /api/v1/admin/orchestration/knowledge/tags/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -270,18 +260,6 @@ describe('PATCH /api/v1/admin/orchestration/knowledge/tags/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makeRequest('PATCH', { name: 'x' }), makeParams(TAG_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.knowledgeTag.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Validation', () => { it('returns 400 for invalid CUID in id param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -381,16 +359,6 @@ describe('PATCH /api/v1/admin/orchestration/knowledge/tags/:id', () => { expect(vi.mocked(invalidateAllAgentAccess)).toHaveBeenCalled(); }); - - it('calls adminLimiter.check on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.knowledgeTag.findUnique).mockResolvedValue(makeTag() as never); - vi.mocked(prisma.knowledgeTag.update).mockResolvedValue(makeTag() as never); - - await PATCH(makeRequest('PATCH', { name: 'x' }), makeParams(TAG_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); }); }); @@ -399,7 +367,6 @@ describe('PATCH /api/v1/admin/orchestration/knowledge/tags/:id', () => { describe('DELETE /api/v1/admin/orchestration/knowledge/tags/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -420,18 +387,6 @@ describe('DELETE /api/v1/admin/orchestration/knowledge/tags/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeRequest('DELETE'), makeParams(TAG_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.knowledgeTag.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Validation', () => { it('returns 400 for invalid CUID in id param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -532,17 +487,5 @@ describe('DELETE /api/v1/admin/orchestration/knowledge/tags/:id', () => { expect(vi.mocked(invalidateAllAgentAccess)).toHaveBeenCalled(); }); - - it('calls adminLimiter.check on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.knowledgeTag.findUnique).mockResolvedValue( - makeTag({ _count: { documents: 0, agents: 0 } }) as never - ); - vi.mocked(prisma.knowledgeTag.delete).mockResolvedValue(makeTag() as never); - - await DELETE(makeRequest('DELETE'), makeParams(TAG_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); }); }); diff --git a/tests/integration/api/v1/admin/orchestration/maintenance.tick.test.ts b/tests/integration/api/v1/admin/orchestration/maintenance.tick.test.ts index 4e693ae63..ba9f9b0ee 100644 --- a/tests/integration/api/v1/admin/orchestration/maintenance.tick.test.ts +++ b/tests/integration/api/v1/admin/orchestration/maintenance.tick.test.ts @@ -35,13 +35,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/models.test.ts b/tests/integration/api/v1/admin/orchestration/models.test.ts index dda10e5f7..50197e4ad 100644 --- a/tests/integration/api/v1/admin/orchestration/models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/models.test.ts @@ -34,13 +34,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/model-registry', () => ({ getAvailableModels: vi.fn(() => []), getRegistryFetchedAt: vi.fn(() => 0), @@ -59,7 +52,6 @@ vi.mock('@/lib/db/client', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getAvailableModels, refreshFromOpenRouter } from '@/lib/orchestration/llm/model-registry'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -79,7 +71,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/models', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -134,15 +125,6 @@ describe('GET /api/v1/admin/orchestration/models', () => { expect(vi.mocked(refreshFromOpenRouter)).toHaveBeenCalledWith(); }); - - it('does NOT call adminLimiter.check on the default path', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(getAvailableModels).mockReturnValue([]); - - await GET(makeGetRequest()); - - expect(vi.mocked(adminLimiter.check)).not.toHaveBeenCalled(); - }); }); describe('Refresh path (?refresh=true)', () => { @@ -166,25 +148,6 @@ describe('GET /api/v1/admin/orchestration/models', () => { // test-review:accept tobe_true — structural boolean assertion on API response field expect(data.data.refreshed).toBe(true); }); - - it('is rate-limited on the refresh path', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest({ refresh: 'true' })); - - expect(response.status).toBe(429); - expect(vi.mocked(refreshFromOpenRouter)).not.toHaveBeenCalled(); - }); - - it('calls adminLimiter.check only on the refresh path', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(getAvailableModels).mockReturnValue([]); - - await GET(makeGetRequest({ refresh: 'true' })); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); }); describe('DB-model merge', () => { diff --git a/tests/integration/api/v1/admin/orchestration/observability.dashboard-stats.test.ts b/tests/integration/api/v1/admin/orchestration/observability.dashboard-stats.test.ts index b05b44a5e..1d74a94ba 100644 --- a/tests/integration/api/v1/admin/orchestration/observability.dashboard-stats.test.ts +++ b/tests/integration/api/v1/admin/orchestration/observability.dashboard-stats.test.ts @@ -49,13 +49,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.bulk.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.bulk.test.ts index 06c76cdfb..07b0ad4e2 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.bulk.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.bulk.test.ts @@ -45,17 +45,9 @@ vi.mock('@/lib/orchestration/llm/provider-selector', () => ({ invalidateModelCache: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - import { POST } from '@/app/api/v1/admin/orchestration/provider-models/bulk/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -100,7 +92,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/provider-models/bulk', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication', () => { @@ -117,15 +108,6 @@ describe('POST /api/v1/admin/orchestration/provider-models/bulk', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await POST(makeRequest({ providerSlug: 'openai', models: [makeRow()] })); - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { it('returns 400 when providerSlug is missing', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.id.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.id.test.ts index 6c54f6814..41d3c3fec 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.id.test.ts @@ -54,13 +54,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-selector', () => ({ invalidateModelCache: vi.fn(), })); @@ -69,7 +62,6 @@ vi.mock('@/lib/orchestration/llm/provider-selector', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -193,7 +185,6 @@ describe('GET /api/v1/admin/orchestration/provider-models/:id', () => { describe('PATCH /api/v1/admin/orchestration/provider-models/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 404 when model not found', async () => { @@ -253,17 +244,6 @@ describe('PATCH /api/v1/admin/orchestration/provider-models/:id', () => { expect(callArgs.data).not.toHaveProperty('isDefault'); }); - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH( - makePatchRequest(MODEL_ID, { name: 'Updated' }), - routeContext(MODEL_ID) - ); - expect(response.status).toBe(429); - }); - // Single-field PATCH cases share an identical shape: mock findUnique // → mock update with one changed field → assert 200 + Prisma update // received that field. it.each keeps the contract surface explicit @@ -457,7 +437,6 @@ describe('PATCH /api/v1/admin/orchestration/provider-models/:id', () => { describe('DELETE /api/v1/admin/orchestration/provider-models/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 404 when model not found', async () => { @@ -485,14 +464,6 @@ describe('DELETE /api/v1/admin/orchestration/provider-models/:id', () => { expect(prisma.aiProviderModel.update).not.toHaveBeenCalled(); }); - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(MODEL_ID), routeContext(MODEL_ID)); - expect(response.status).toBe(429); - }); - it('returns 409 with bound agents when an active agent uses the model', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiProviderModel.findUnique).mockResolvedValue( diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts index 48f007fca..1b598c810 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts @@ -77,7 +77,7 @@ vi.mock('@/lib/orchestration/settings', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, apiLimiter } from '@/lib/security/rate-limit'; +import { apiLimiter } from '@/lib/security/rate-limit'; import { getOrchestrationSettings } from '@/lib/orchestration/settings'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -515,7 +515,6 @@ describe('GET /api/v1/admin/orchestration/provider-models', () => { describe('POST /api/v1/admin/orchestration/provider-models', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); const validBody = { @@ -582,12 +581,4 @@ describe('POST /api/v1/admin/orchestration/provider-models', () => { expect(body.success).toBe(false); expect(body.error.code).toBe('CONFLICT'); }); - - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(validBody)); - expect(response.status).toBe(429); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/providers.id.health.test.ts b/tests/integration/api/v1/admin/orchestration/providers.id.health.test.ts index efcb4af64..6b2b7f3fe 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.id.health.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.id.health.test.ts @@ -34,16 +34,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json( - { success: false, error: { code: 'RATE_LIMITED', message: 'rate limited' } }, - { status: 429 } - ) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); @@ -76,7 +66,6 @@ vi.mock('@/lib/orchestration/llm/circuit-breaker', () => { import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getCircuitBreakerStatus } from '@/lib/orchestration/llm/circuit-breaker'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -190,12 +179,6 @@ describe('GET /api/v1/admin/orchestration/providers/:id/health', () => { describe('POST /api/v1/admin/orchestration/providers/:id/health', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: Date.now() + 60_000, - }); }); it('returns 401 when unauthenticated', async () => { @@ -229,17 +212,4 @@ describe('POST /api/v1/admin/orchestration/providers/:id/health', () => { expect(body.data.providerId).toBe(PROVIDER_ID); expect(body.data.slug).toBe('anthropic'); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const res = await POST(makeRequest('POST'), makeParams()); - expect(res.status).toBe(429); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/providers.id.models.test.ts b/tests/integration/api/v1/admin/orchestration/providers.id.models.test.ts index b6b301e34..c6d9a3331 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.id.models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.id.models.test.ts @@ -54,13 +54,6 @@ vi.mock('@/lib/orchestration/llm/model-registry', () => ({ refreshFromOpenRouter: vi.fn(() => Promise.resolve()), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // Default-models enrichment pulls from the settings singleton. Stub // it with an empty merged map by default so existing tests don't see // any default-role badges; the dedicated test below overrides this. @@ -78,7 +71,6 @@ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { getProvider, isApiKeyEnvVarSet } from '@/lib/orchestration/llm/provider-manager'; import { refreshFromOpenRouter } from '@/lib/orchestration/llm/model-registry'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getOrchestrationSettings } from '@/lib/orchestration/settings'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -142,16 +134,6 @@ describe('GET /api/v1/admin/orchestration/providers/:id/models', () => { vi.clearAllMocks(); // Default: API key is present. Individual tests override when needed. vi.mocked(isApiKeyEnvVarSet).mockReturnValue(true); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); - }); - - describe('Rate limiting', () => { - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - const response = await GET(makeGetRequest(), makeParams(PROVIDER_ID)); - expect(response.status).toBe(429); - }); }); describe('Authentication & Authorization', () => { diff --git a/tests/integration/api/v1/admin/orchestration/providers.id.test-connection.test.ts b/tests/integration/api/v1/admin/orchestration/providers.id.test-connection.test.ts index 78d35d00b..035aa3cc8 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.id.test-connection.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.id.test-connection.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ testProvider: vi.fn(), })); @@ -55,7 +48,6 @@ vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { testProvider } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -106,7 +98,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/providers/:id/test', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -148,17 +139,6 @@ describe('POST /api/v1/admin/orchestration/providers/:id/test', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeParams(PROVIDER_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('testProvider succeeds', () => { it('returns HTTP 200 when testProvider returns { ok: true }', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/providers.id.test-model.test.ts b/tests/integration/api/v1/admin/orchestration/providers.id.test-model.test.ts index f959af87a..ddca53077 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.id.test-model.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.id.test-model.test.ts @@ -33,13 +33,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -65,7 +58,6 @@ vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getProvider } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -152,7 +144,6 @@ function makeMockProvider( describe('POST /api/v1/admin/orchestration/providers/:id/test-model', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -173,17 +164,6 @@ describe('POST /api/v1/admin/orchestration/providers/:id/test-model', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest(), makeParams()); - - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { it('returns 400 when id is not a valid CUID', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/providers.id.test.ts b/tests/integration/api/v1/admin/orchestration/providers.id.test.ts index 7a5c54ef2..9a9cccd40 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.id.test.ts @@ -48,13 +48,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ isApiKeyEnvVarSet: vi.fn(() => false), clearCache: vi.fn(), @@ -69,7 +62,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { isApiKeyEnvVarSet, clearCache } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -193,7 +185,6 @@ describe('GET /api/v1/admin/orchestration/providers/:id', () => { describe('PATCH /api/v1/admin/orchestration/providers/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -220,33 +211,6 @@ describe('PATCH /api/v1/admin/orchestration/providers/:id', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on PATCH (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiProviderConfig.findUnique).mockResolvedValue(makeProvider() as never); - vi.mocked(prisma.aiProviderConfig.update).mockResolvedValue( - makeProvider({ name: 'Updated' }) as never - ); - - await PATCH(makeRequest('PATCH', { name: 'Updated' }), makeParams(PROVIDER_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH( - makeRequest('PATCH', { name: 'Updated' }), - makeParams(PROVIDER_ID) - ); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiProviderConfig.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful update', () => { it('updates provider and returns 200 with apiKeyPresent field', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -409,7 +373,6 @@ describe('PATCH /api/v1/admin/orchestration/providers/:id', () => { describe('DELETE /api/v1/admin/orchestration/providers/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -430,18 +393,6 @@ describe('DELETE /api/v1/admin/orchestration/providers/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeRequest('DELETE'), makeParams(PROVIDER_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiProviderConfig.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful soft delete', () => { it('sets isActive to false and returns success', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -502,7 +453,6 @@ describe('DELETE /api/v1/admin/orchestration/providers/:id?permanent=true', () = beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Default to "no references found" — individual tests override. vi.mocked(prisma.aiAgent.count).mockResolvedValue(0); vi.mocked(prisma.aiCostLog.count).mockResolvedValue(0); diff --git a/tests/integration/api/v1/admin/orchestration/providers.test-bulk.test.ts b/tests/integration/api/v1/admin/orchestration/providers.test-bulk.test.ts index 5d99bd3ae..193856c24 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.test-bulk.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.test-bulk.test.ts @@ -50,13 +50,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ testProvider: vi.fn(), })); @@ -65,7 +58,6 @@ vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { testProvider } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -115,7 +107,6 @@ interface ErrorBody { describe('POST /api/v1/admin/orchestration/providers/test-bulk', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -136,20 +127,6 @@ describe('POST /api/v1/admin/orchestration/providers/test-bulk', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when the admin limiter rejects the call', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - retryAfter: 60, - } as never); - - const response = await POST(makePostRequest({ providerIds: [PROVIDER_A] })); - - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { beforeEach(() => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/providers.test.ts b/tests/integration/api/v1/admin/orchestration/providers.test.ts index f8b946814..12ff7db8b 100644 --- a/tests/integration/api/v1/admin/orchestration/providers.test.ts +++ b/tests/integration/api/v1/admin/orchestration/providers.test.ts @@ -42,13 +42,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ isApiKeyEnvVarSet: vi.fn(() => false), clearCache: vi.fn(), @@ -63,7 +56,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { isApiKeyEnvVarSet } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -259,7 +251,6 @@ describe('GET /api/v1/admin/orchestration/providers', () => { describe('POST /api/v1/admin/orchestration/providers', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -280,17 +271,6 @@ describe('POST /api/v1/admin/orchestration/providers', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_PROVIDER)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful creation', () => { it('creates provider and returns 201 with apiKeyPresent field', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/quiz-scores.test.ts b/tests/integration/api/v1/admin/orchestration/quiz-scores.test.ts index f83efd627..918b25607 100644 --- a/tests/integration/api/v1/admin/orchestration/quiz-scores.test.ts +++ b/tests/integration/api/v1/admin/orchestration/quiz-scores.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -66,7 +59,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -204,7 +196,6 @@ describe('GET /api/v1/admin/orchestration/quiz-scores', () => { describe('POST /api/v1/admin/orchestration/quiz-scores', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -225,17 +216,6 @@ describe('POST /api/v1/admin/orchestration/quiz-scores', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest()); - - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { it('returns 400 when correct is missing from body', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/settings.test.ts b/tests/integration/api/v1/admin/orchestration/settings.test.ts index 7995a0d30..5522f1f72 100644 --- a/tests/integration/api/v1/admin/orchestration/settings.test.ts +++ b/tests/integration/api/v1/admin/orchestration/settings.test.ts @@ -59,16 +59,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json( - { success: false, error: { code: 'RATE_LIMITED', message: 'rate limited' } }, - { status: 429 } - ) - ), -})); - vi.mock('@/lib/orchestration/llm/model-registry', async (importOriginal) => { const actual = await importOriginal(); return { @@ -120,7 +110,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { invalidateSettingsCache } from '@/lib/orchestration/llm/settings-resolver'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -199,12 +188,6 @@ async function parseJson(res: Response): Promise { describe('Admin Orchestration — /settings', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: Date.now() + 60_000, - }); }); describe('GET — Authentication & Authorization', () => { @@ -271,24 +254,6 @@ describe('Admin Orchestration — /settings', () => { }); }); - describe('GET — Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const res = await GET(makeGet()); - - expect(res.status).toBe(429); - // Upsert must NOT fire when rate-limited - expect(prisma.aiOrchestrationSettings.upsert).not.toHaveBeenCalled(); - }); - }); - describe('GET — Malformed stored defaultModels', () => { // Exercises parseStoredDefaults() fail branch: whatever the stored JSON // is (string, array, null, nested object with non-string values), it @@ -1100,20 +1065,4 @@ describe('Admin Orchestration — /settings', () => { expect(upsertCall?.update.embedAllowedOrigins).toEqual([]); }); }); - - describe('PATCH — Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const res = await PATCH(makePatch({ globalMonthlyBudgetUsd: 50 })); - - expect(res.status).toBe(429); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts b/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts index 98f1cea3f..a13036c17 100644 --- a/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts @@ -54,13 +54,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -87,7 +80,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -159,7 +151,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/webhooks/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -251,7 +242,6 @@ describe('GET /api/v1/admin/orchestration/webhooks/:id', () => { describe('PATCH /api/v1/admin/orchestration/webhooks/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -272,17 +262,6 @@ describe('PATCH /api/v1/admin/orchestration/webhooks/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest(), makeParams(WEBHOOK_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful update', () => { it('returns 200 with the updated webhook', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -367,7 +346,6 @@ describe('PATCH /api/v1/admin/orchestration/webhooks/:id', () => { describe('DELETE /api/v1/admin/orchestration/webhooks/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -388,17 +366,6 @@ describe('DELETE /api/v1/admin/orchestration/webhooks/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeParams(WEBHOOK_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful deletion', () => { it('returns 200 with { deleted: true }', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts b/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts index 66e63a7cd..3821c561b 100644 --- a/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts +++ b/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts @@ -46,22 +46,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json( - { - success: false, - error: { - code: 'RATE_LIMIT_EXCEEDED', - message: 'Too many requests. Please try again later.', - }, - }, - { status: 429 } - ) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/api/context', () => ({ @@ -83,7 +67,6 @@ vi.mock('@/lib/env', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ───────────────────────────────────────────────────────────────── @@ -131,7 +114,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/webhooks/:id/test', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.spyOn(global, 'fetch').mockResolvedValue(new Response(null, { status: 200 }) as never); }); @@ -187,32 +169,6 @@ describe('POST /api/v1/admin/orchestration/webhooks/:id/test', () => { // ─── Rate Limiting ───────────────────────────────────────────────────────── - describe('Rate limiting', () => { - it('returns 429 when adminLimiter is exhausted before DB is consulted', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const response = await POST(makeRequest(), makeParams(WEBHOOK_ID)); - const body = await parseJson<{ success: boolean; error: { code: string; message: string } }>( - response - ); - - // Assert — rate limit fires before any DB call; envelope matches the - // shape `createRateLimitResponse` produces in `lib/security/rate-limit.ts`. - expect(response.status).toBe(429); - expect(body).toEqual({ - success: false, - error: { - code: 'RATE_LIMIT_EXCEEDED', - message: 'Too many requests. Please try again later.', - }, - }); - expect(vi.mocked(prisma.aiWebhookSubscription.findFirst)).not.toHaveBeenCalled(); - }); - }); - // ─── Validation ─────────────────────────────────────────────────────────── describe('Input validation', () => { diff --git a/tests/integration/api/v1/admin/orchestration/workflows.id.cost-estimate.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.id.cost-estimate.test.ts index 8cca1d60a..578694825 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.id.cost-estimate.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.id.cost-estimate.test.ts @@ -41,20 +41,12 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/cost-estimation/workflow-cost', () => ({ estimateWorkflowCost: vi.fn(), })); import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { estimateWorkflowCost } from '@/lib/orchestration/cost-estimation/workflow-cost'; import { GET, POST } from '@/app/api/v1/admin/orchestration/workflows/[id]/cost-estimate/route'; import type { WorkflowDefinition } from '@/types/orchestration'; @@ -105,7 +97,6 @@ const SAMPLE_ESTIMATE = { describe('GET /api/v1/admin/orchestration/workflows/:id/cost-estimate', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(estimateWorkflowCost).mockResolvedValue(SAMPLE_ESTIMATE); // resolveEffectiveCap reads the singleton settings row; default to // no org-level cap configured so existing assertions don't need to @@ -264,7 +255,6 @@ function makeInvalidJsonRequest(): NextRequest { describe('POST /api/v1/admin/orchestration/workflows/:id/cost-estimate', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(estimateWorkflowCost).mockResolvedValue(SAMPLE_ESTIMATE); vi.mocked(prisma.aiOrchestrationSettings.findUnique).mockResolvedValue({ defaultMaxCostPerExecutionUsd: null, @@ -458,24 +448,4 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/cost-estimate', () => { expect(body.data.effectiveCapUsd).toBeNull(); }); }); - - describe('Rate limiting', () => { - beforeEach(() => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - }); - - it('returns 429 when rate-limited and calls adminLimiter.check', async () => { - const { createRateLimitResponse } = await import('@/lib/security/rate-limit'); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST( - makePostRequest({ definition: MINIMAL_DEFINITION }), - makeParams(WORKFLOW_ID) - ); - expect(response.status).toBe(429); - // Verify the limiter was consulted — not just that a 429 appeared - expect(adminLimiter.check).toHaveBeenCalledOnce(); - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - }); - }); }); diff --git a/tests/integration/api/v1/admin/orchestration/workflows.id.dry-run.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.id.dry-run.test.ts index 212057672..f5fd1f5ec 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.id.dry-run.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.id.dry-run.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -70,7 +63,6 @@ vi.mock('@/lib/orchestration/workflows', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { validateWorkflow, semanticValidateWorkflow, @@ -149,7 +141,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/workflows/:id/dry-run', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(validateWorkflow).mockReturnValue({ ok: true, errors: [] }); vi.mocked(semanticValidateWorkflow).mockResolvedValue({ ok: true, errors: [] }); vi.mocked(extractTemplateVariables).mockReturnValue([]); @@ -173,17 +164,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/dry-run', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makeRequest(), makeParams()); - - expect(response.status).toBe(429); - }); - }); - describe('Validation', () => { it('returns 400 when id is not a valid CUID', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/workflows.id.execute.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.id.execute.test.ts index 68ea77782..3d1f9e179 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.id.execute.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.id.execute.test.ts @@ -41,13 +41,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // Mock the engine module so we can assert the route wires it up // correctly without pulling in every executor + provider transitively. const mockExecute = vi.fn(); @@ -66,7 +59,6 @@ vi.mock('@/lib/orchestration/engine/orchestration-engine', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -158,7 +150,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/workflows/:id/execute', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -179,17 +170,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/execute', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when adminLimiter blocks the request', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeParams(WORKFLOW_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('CUID validation', () => { it('returns 400 for invalid CUID param', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/workflows.id.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.id.test.ts index e50593922..db9bbfc31 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.id.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), @@ -54,7 +47,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -183,7 +175,6 @@ describe('GET /api/v1/admin/orchestration/workflows/:id', () => { describe('PATCH /api/v1/admin/orchestration/workflows/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -210,33 +201,6 @@ describe('PATCH /api/v1/admin/orchestration/workflows/:id', () => { }); }); - describe('Rate limiting', () => { - it('calls adminLimiter.check on PATCH (mutating route)', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(prisma.aiWorkflow.findUnique).mockResolvedValue(makeWorkflow() as never); - vi.mocked(prisma.aiWorkflow.update).mockResolvedValue( - makeWorkflow({ name: 'Updated' }) as never - ); - - await PATCH(makeRequest('PATCH', { name: 'Updated' }), makeParams(WORKFLOW_ID)); - - expect(vi.mocked(adminLimiter.check)).toHaveBeenCalledOnce(); - }); - - it('returns 429 when rate limit exceeded on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH( - makeRequest('PATCH', { name: 'Updated' }), - makeParams(WORKFLOW_ID) - ); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiWorkflow.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful update', () => { it('updates workflow and returns 200', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -367,7 +331,6 @@ describe('PATCH /api/v1/admin/orchestration/workflows/:id', () => { describe('DELETE /api/v1/admin/orchestration/workflows/:id', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -388,18 +351,6 @@ describe('DELETE /api/v1/admin/orchestration/workflows/:id', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit exceeded on DELETE', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeRequest('DELETE'), makeParams(WORKFLOW_ID)); - - expect(response.status).toBe(429); - expect(vi.mocked(prisma.aiWorkflow.findUnique)).not.toHaveBeenCalled(); - }); - }); - describe('Successful soft delete', () => { it('sets isActive to false and returns success', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/workflows.id.validate.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.id.validate.test.ts index 7e04613c9..a34e1a1ce 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.id.validate.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.id.validate.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // NOTE: validateWorkflow is NOT mocked — we test against the real implementation // to verify the full integration between the route and the validator. @@ -57,7 +50,6 @@ vi.mock('@/lib/orchestration/workflows/semantic-validator', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -161,7 +153,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/workflows/:id/validate', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -280,17 +271,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/validate', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(), makeParams(WORKFLOW_ID)); - - expect(response.status).toBe(429); - }); - }); - describe('Malformed workflow definition', () => { it('returns 400 with VALIDATION_ERROR and issue messages when workflowDefinition fails schema parse', async () => { // Arrange: workflow exists but its stored definition is structurally invalid diff --git a/tests/integration/api/v1/admin/orchestration/workflows.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.test.ts index 8c151f4c7..9c0dd2d92 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.test.ts @@ -58,13 +58,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(), @@ -74,7 +67,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -246,7 +238,6 @@ describe('GET /api/v1/admin/orchestration/workflows', () => { describe('POST /api/v1/admin/orchestration/workflows', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -267,17 +258,6 @@ describe('POST /api/v1/admin/orchestration/workflows', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_WORKFLOW)); - - expect(response.status).toBe(429); - }); - }); - describe('Successful creation', () => { it('creates workflow and returns 201', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/integration/api/v1/admin/orchestration/workflows.versioning.test.ts b/tests/integration/api/v1/admin/orchestration/workflows.versioning.test.ts index 98762aeb9..026f7325d 100644 --- a/tests/integration/api/v1/admin/orchestration/workflows.versioning.test.ts +++ b/tests/integration/api/v1/admin/orchestration/workflows.versioning.test.ts @@ -63,13 +63,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), computeChanges: vi.fn(() => null), @@ -84,7 +77,6 @@ vi.mock('@/lib/orchestration/workflows/semantic-validator', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { semanticValidateWorkflow } from '@/lib/orchestration/workflows/semantic-validator'; @@ -172,12 +164,6 @@ function makeParams(workflowId: string = WORKFLOW_ID) { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: 9999999999, - }); vi.mocked(semanticValidateWorkflow).mockResolvedValue({ ok: true, errors: [] }); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); }); @@ -384,20 +370,6 @@ describe('POST /workflows/:id/publish', () => { expect(res.status).toBe(403); }); - it('429s when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60000, - }); - const res = await PUBLISH( - makeRequest('POST', `/api/v1/admin/orchestration/workflows/${WORKFLOW_ID}/publish`, {}), - makeParams() - ); - expect(res.status).toBe(429); - }); - it('400s for an invalid workflow id', async () => { const res = await PUBLISH( makeRequest('POST', `/api/v1/admin/orchestration/workflows/not-a-cuid/publish`, {}), @@ -438,20 +410,6 @@ describe('POST /workflows/:id/discard-draft', () => { expect(res.status).toBe(401); }); - it('429s when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - }); - const res = await DISCARD( - makeRequest('POST', `/api/v1/admin/orchestration/workflows/${WORKFLOW_ID}/discard-draft`), - makeParams() - ); - expect(res.status).toBe(429); - }); - it('400s for an invalid workflow id', async () => { const res = await DISCARD( makeRequest('POST', `/api/v1/admin/orchestration/workflows/not-a-cuid/discard-draft`), @@ -565,22 +523,6 @@ describe('POST /workflows/:id/rollback', () => { expect(res.status).toBe(400); }); - it('429s when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - }); - const res = await ROLLBACK( - makeRequest('POST', `/api/v1/admin/orchestration/workflows/${WORKFLOW_ID}/rollback`, { - targetVersionId: VERSION_ID_V1, - }), - makeParams() - ); - expect(res.status).toBe(429); - }); - it('400s for an invalid workflow id', async () => { const res = await ROLLBACK( makeRequest('POST', `/api/v1/admin/orchestration/workflows/not-a-cuid/rollback`, { @@ -646,20 +588,6 @@ describe('GET /workflows/:id/versions', () => { ); expect(res.status).toBe(404); }); - - it('429s when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - }); - const res = await LIST_VERSIONS( - makeRequest('GET', `/api/v1/admin/orchestration/workflows/${WORKFLOW_ID}/versions`), - makeParams() - ); - expect(res.status).toBe(429); - }); }); // ─── GET /versions/:version ──────────────────────────────────────────────── @@ -694,18 +622,4 @@ describe('GET /workflows/:id/versions/:version', () => { ); expect(res.status).toBe(400); }); - - it('429s when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - }); - const res = await GET_VERSION( - makeRequest('GET', `/api/v1/admin/orchestration/workflows/${WORKFLOW_ID}/versions/1`), - { params: Promise.resolve({ id: WORKFLOW_ID, version: '1' }) } - ); - expect(res.status).toBe(429); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.test.ts index 7fae3a4bb..2586ac48f 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route.test.ts @@ -32,13 +32,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -52,7 +45,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { POST } from '@/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/restore/route'; @@ -127,7 +119,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('POST /agents/:id/versions/:versionId/restore', () => { @@ -150,27 +141,6 @@ describe('POST /agents/:id/versions/:versionId/restore', () => { // ── Rate limiting ────────────────────────────────────────────────── - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await POST( - makeRequest(AGENT_ID, VERSION_ID), - makeParams(AGENT_ID, VERSION_ID) - ); - - // Assert - expect(response.status).toBe(429); - expect(prisma.$transaction).not.toHaveBeenCalled(); - }); - // ── Validation ───────────────────────────────────────────────────── it('returns 400 for invalid agent CUID', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/agent-system-protection.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/agent-system-protection.route.test.ts index 56268cd4d..c9dd69771 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/agent-system-protection.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/agent-system-protection.route.test.ts @@ -50,13 +50,6 @@ vi.mock('@/lib/db/client', () => { return { prisma: mock }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/capabilities-usage.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/capabilities-usage.route.test.ts index 21128c485..f27de27a8 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/capabilities-usage.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/capabilities-usage.route.test.ts @@ -42,13 +42,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -57,7 +50,6 @@ vi.mock('@/lib/security/ip', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Fixtures ───────────────────────────────────────────────────────────────── @@ -87,7 +79,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/agents/:id/capabilities/usage', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Authentication & Authorization ───────────────────────────────────────── @@ -236,18 +227,4 @@ describe('GET /api/v1/admin/orchestration/agents/:id/capabilities/usage', () => }); // ── Rate limiting ────────────────────────────────────────────────────────── - - describe('Rate limiting', () => { - it('returns 429 when the rate limit is exceeded', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const response = await GET(makeGetRequest(), makeParams(AGENT_ID)); - - // Assert - expect(response.status).toBe(429); - }); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/[tokenId]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/[tokenId]/route.test.ts index a53d83882..42e18b5c9 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/[tokenId]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/[tokenId]/route.test.ts @@ -28,11 +28,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/route.test.ts index 6be9976b8..e829bdcab 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/embed-tokens/route.test.ts @@ -28,11 +28,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/invite-tokens/invite-tokens.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/invite-tokens/invite-tokens.route.test.ts index cda832e7d..266971afa 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/invite-tokens/invite-tokens.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/invite-tokens/invite-tokens.route.test.ts @@ -34,13 +34,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -56,7 +49,6 @@ import { GET, POST } from '@/app/api/v1/admin/orchestration/agents/[id]/invite-t import { DELETE } from '@/app/api/v1/admin/orchestration/agents/[id]/invite-tokens/[tokenId]/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -100,7 +92,6 @@ describe('Invite Token Endpoints', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── GET — List tokens ────────────────────────────────────────────────── @@ -146,20 +137,6 @@ describe('Invite Token Endpoints', () => { expect(res.status).toBe(401); }); - it('returns 429 when rate limited on GET', async () => { - // Arrange: rate limit exceeded - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await GET(makeGetRequest(), makeAgentParams()); - - expect(res.status).toBe(429); - }); - it('returns 400 for invalid agent id (non-CUID) on GET', async () => { // Arrange: non-CUID agent id const res = await GET(makeGetRequest(), { params: Promise.resolve({ id: 'bad-id' }) }); @@ -217,20 +194,6 @@ describe('Invite Token Endpoints', () => { expect(res.status).toBe(404); }); - it('returns 429 when rate limited on POST', async () => { - // Arrange: rate limit exceeded - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await POST(makePostRequest({}), makeAgentParams()); - - expect(res.status).toBe(429); - }); - it('returns 400 for invalid agent id (non-CUID) on POST', async () => { // Arrange: non-CUID agent id const res = await POST(makePostRequest({}), { params: Promise.resolve({ id: 'bad-id' }) }); @@ -305,18 +268,5 @@ describe('Invite Token Endpoints', () => { expect(res.status).toBe(404); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await DELETE(makeDeleteRequest(), makeTokenParams()); - - expect(res.status).toBe(429); - }); }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/versions/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/versions/route.test.ts index 081cfcafa..4de2ce033 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/versions/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/versions/route.test.ts @@ -35,13 +35,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -55,7 +48,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { GET as ListVersions } from '@/app/api/v1/admin/orchestration/agents/[id]/versions/route'; import { GET as GetVersion } from '@/app/api/v1/admin/orchestration/agents/[id]/versions/[versionId]/route'; @@ -124,7 +116,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); // Reset rate limiter to allow-by-default after each test - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /agents/:id/versions (list)', () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/agents/widget-config/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/agents/widget-config/route.test.ts index 27e8c8509..9a44807ad 100644 --- a/tests/unit/app/api/v1/admin/orchestration/agents/widget-config/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/agents/widget-config/route.test.ts @@ -22,16 +22,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => - new Response(JSON.stringify({ success: false, error: { code: 'RATE_LIMITED' } }), { - status: 429, - }) - ), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); @@ -39,7 +29,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { GET, PATCH } from '@/app/api/v1/admin/orchestration/agents/[id]/widget-config/route'; import { DEFAULT_WIDGET_CONFIG } from '@/lib/validations/orchestration'; @@ -123,13 +112,6 @@ describe('GET /agents/:id/widget-config', () => { expect(body.data.config.primaryColor).toBe('#16a34a'); expect(body.data.config.headerTitle).toBe('Council'); }); - - it('returns 429 when rate-limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValueOnce({ success: false } as never); - const response = await GET(makeGetRequest(), makeParams()); - expect(response.status).toBe(429); - }); }); describe('PATCH /agents/:id/widget-config', () => { @@ -217,11 +199,4 @@ describe('PATCH /agents/:id/widget-config', () => { ); expect(response.status).toBe(400); }); - - it('returns 429 when rate-limited on PATCH', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValueOnce({ success: false } as never); - const response = await PATCH(makePatchRequest({ primaryColor: '#16a34a' }), makeParams()); - expect(response.status).toBe(429); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/analytics/analytics.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/analytics/analytics.route.test.ts index 7ab616b53..24737dd6b 100644 --- a/tests/unit/app/api/v1/admin/orchestration/analytics/analytics.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/analytics/analytics.route.test.ts @@ -27,13 +27,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -54,7 +47,6 @@ import { GET as getEngagement } from '@/app/api/v1/admin/orchestration/analytics import { GET as getGaps } from '@/app/api/v1/admin/orchestration/analytics/content-gaps/route'; import { GET as getFeedback } from '@/app/api/v1/admin/orchestration/analytics/feedback/route'; import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getPopularTopics, getUnansweredQuestions, @@ -80,7 +72,6 @@ describe('Analytics API', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Topics Endpoint ───────────────────────────────────────────────────── @@ -120,19 +111,6 @@ describe('Analytics API', () => { expect(res.status).toBe(401); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getTopics(makeGetRequest()); - - expect(res.status).toBe(429); - }); }); // ── Unanswered Endpoint ───────────────────────────────────────────────── @@ -164,19 +142,6 @@ describe('Analytics API', () => { expect(res.status).toBe(401); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getUnanswered(makeGetRequest()); - - expect(res.status).toBe(429); - }); }); // ── Engagement Endpoint ───────────────────────────────────────────────── @@ -238,19 +203,6 @@ describe('Analytics API', () => { expect(res.status).toBe(401); }); - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getGaps(makeGetRequest()); - - expect(res.status).toBe(429); - }); - it('passes agentId query param to service', async () => { vi.mocked(getContentGaps).mockResolvedValue([]); @@ -297,19 +249,6 @@ describe('Analytics API', () => { expect(res.status).toBe(401); }); - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getFeedback(makeGetRequest()); - - expect(res.status).toBe(429); - }); - it('passes date range query params to feedback service', async () => { vi.mocked(getFeedbackSummary).mockResolvedValue({ overall: { thumbsUp: 0, thumbsDown: 0, total: 0, satisfactionRate: null }, @@ -368,19 +307,6 @@ describe('Analytics API', () => { expect(res.status).toBe(401); }); - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getEngagement(makeGetRequest()); - - expect(res.status).toBe(429); - }); - it('passes date range query params to engagement service', async () => { vi.mocked(getEngagementMetrics).mockResolvedValue({ totalConversations: 0, diff --git a/tests/unit/app/api/v1/admin/orchestration/audit-log/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/audit-log/route.test.ts index d46217db4..6446036da 100644 --- a/tests/unit/app/api/v1/admin/orchestration/audit-log/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/audit-log/route.test.ts @@ -26,13 +26,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 0 })), - }, - createRateLimitResponse: vi.fn(), -})); - // ─── Imports ──────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; @@ -317,37 +310,4 @@ describe('GET /audit-log', () => { expect(response.status).toBe(400); }); - - it('returns 429 when rate limited', async () => { - const { adminLimiter, createRateLimitResponse } = await import('@/lib/security/rate-limit'); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - vi.mocked(createRateLimitResponse).mockReturnValue( - new Response(null, { status: 429 }) as never - ); - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(429); - }); - - it('allows the request through when rate limiter reports success', async () => { - const { adminLimiter } = await import('@/lib/security/rate-limit'); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: Date.now() + 60_000, - }); - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(200); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/capabilities/capability-system-protection.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/capabilities/capability-system-protection.route.test.ts index ceb8a981d..1c8df67b5 100644 --- a/tests/unit/app/api/v1/admin/orchestration/capabilities/capability-system-protection.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/capabilities/capability-system-protection.route.test.ts @@ -44,13 +44,6 @@ vi.mock('@/lib/orchestration/capabilities', () => ({ capabilityDispatcher: { clearCache: vi.fn() }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts index 54154887c..49d7d9fd0 100644 --- a/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts @@ -88,7 +88,6 @@ vi.mock('@/lib/logging/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { - adminLimiter, agentChatLimiter, chatLimiter, createRateLimitResponse, @@ -161,7 +160,6 @@ describe('POST /api/v1/admin/orchestration/chat/stream', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(createAdminSession()); - vi.mocked(adminLimiter.check).mockReturnValue(makeRateLimitResult(true)); vi.mocked(chatLimiter.check).mockReturnValue(makeRateLimitResult(true)); // Per-agent rate-limit lookup — return a generic agent row so the // route progresses to streamChat. Individual tests can override. @@ -226,17 +224,6 @@ describe('POST /api/v1/admin/orchestration/chat/stream', () => { ); }); - it('returns 429 when admin IP rate limit is exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue(makeRateLimitResult(false)); - - const req = createMockRequest(validPayload); - const response = await POST(req); - - expect(response.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(streamChat).not.toHaveBeenCalled(); - }); - it('returns 429 when chat user rate limit is exceeded', async () => { vi.mocked(chatLimiter.check).mockReturnValue(makeRateLimitResult(false)); diff --git a/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts index 0e6237f00..0712d60c1 100644 --- a/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts @@ -26,11 +26,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => new Response('Rate limited', { status: 429 })), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -48,7 +43,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { GET as ExportConversations } from '@/app/api/v1/admin/orchestration/conversations/export/route'; @@ -96,7 +90,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /conversations/export', () => { @@ -135,15 +128,6 @@ describe('GET /conversations/export', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limited', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await ExportConversations(makeRequest()); - - expect(response.status).toBe(429); - }); - it('returns JSON export by default', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.aiConversation.findMany).mockResolvedValue([makeConversation()] as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/conversations/search/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/conversations/search/route.test.ts index a00dd7eac..b76129b10 100644 --- a/tests/unit/app/api/v1/admin/orchestration/conversations/search/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/conversations/search/route.test.ts @@ -23,11 +23,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/orchestration/knowledge/embedder', () => ({ embedText: vi.fn(), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/costs/alerts/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/costs/alerts/route.test.ts index c614baf94..801751e9a 100644 --- a/tests/unit/app/api/v1/admin/orchestration/costs/alerts/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/costs/alerts/route.test.ts @@ -19,13 +19,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -51,7 +44,6 @@ vi.mock('@/lib/orchestration/llm/cost-reports', () => ({ // ─── Imports ──────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getBudgetAlerts, getGlobalCapStatus } from '@/lib/orchestration/llm/cost-reports'; import { mockAdminUser, @@ -74,7 +66,6 @@ describe('GET /api/v1/admin/orchestration/costs/alerts', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(getBudgetAlerts).mockResolvedValue([]); vi.mocked(getGlobalCapStatus).mockResolvedValue({ cap: null, spent: 0, exceeded: false }); }); @@ -95,17 +86,6 @@ describe('GET /api/v1/admin/orchestration/costs/alerts', () => { // ── Rate limiting ───────────────────────────────────────────────────── - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const response = await GET(makeRequest()); - expect(response.status).toBe(429); - }); - // ── Happy path ──────────────────────────────────────────────────────── it('returns alerts from getBudgetAlerts', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/costs/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/costs/route.test.ts index 1303b4ed8..8ba25000f 100644 --- a/tests/unit/app/api/v1/admin/orchestration/costs/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/costs/route.test.ts @@ -19,13 +19,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -50,7 +43,6 @@ vi.mock('@/lib/orchestration/llm/cost-reports', () => ({ // ─── Imports ──────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getCostBreakdown } from '@/lib/orchestration/llm/cost-reports'; import { mockAdminUser, @@ -81,7 +73,6 @@ describe('GET /api/v1/admin/orchestration/costs', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(getCostBreakdown).mockResolvedValue({ groupBy: 'day', rows: [], @@ -105,17 +96,6 @@ describe('GET /api/v1/admin/orchestration/costs', () => { // ── Rate limiting ───────────────────────────────────────────────────── - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const response = await GET(makeRequest(validParams)); - expect(response.status).toBe(429); - }); - // ── Happy path ──────────────────────────────────────────────────────── it('returns breakdown data for valid query params', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/costs/summary/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/costs/summary/route.test.ts index 9442d7466..3c7e78935 100644 --- a/tests/unit/app/api/v1/admin/orchestration/costs/summary/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/costs/summary/route.test.ts @@ -26,13 +26,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -45,7 +38,6 @@ vi.mock('@/lib/orchestration/llm/cost-reports', () => ({ import { GET } from '@/app/api/v1/admin/orchestration/costs/summary/route'; import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getCostSummary } from '@/lib/orchestration/llm/cost-reports'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { computeETag } from '@/lib/api/etag'; @@ -80,7 +72,6 @@ describe('GET /api/v1/admin/orchestration/costs/summary', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(getCostSummary).mockResolvedValue(mockSummary as never); }); @@ -179,23 +170,4 @@ describe('GET /api/v1/admin/orchestration/costs/summary', () => { }); // ── Rate limiting ──────────────────────────────────────────────────────── - - describe('Rate limiting', () => { - it('returns 429 when rate limit is exceeded', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const res = await GET(makeGetRequest()); - - // Assert - expect(res.status).toBe(429); - expect(getCostSummary).not.toHaveBeenCalled(); - }); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.test.ts index f673a05fb..1d863dc2a 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/[id]/force-fail/route.test.ts @@ -28,12 +28,6 @@ vi.mock('@/lib/auth/config', () => ({ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/db/client', () => ({ prisma: { @@ -56,7 +50,6 @@ vi.mock('@/lib/orchestration/hooks/registry', () => ({ import { POST } from '@/app/api/v1/admin/orchestration/executions/[id]/force-fail/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { recordForceFailEvent } from '@/lib/orchestration/engine/lease'; import { emitHookEvent } from '@/lib/orchestration/hooks/registry'; @@ -127,7 +120,6 @@ describe('POST /executions/:id/force-fail', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── 1. Unauthenticated ──────────────────────────────────────────────────── @@ -172,22 +164,6 @@ describe('POST /executions/:id/force-fail', () => { // ── 3. Rate limited ─────────────────────────────────────────────────────── - it('returns 429 when the admin rate limiter rejects the request', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now(), - } as never); - vi.mocked(createRateLimitResponse).mockReturnValue( - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ); - - const res = await POST(makeRequest(), makeContext()); - - expect(res.status).toBe(429); - }); - // ── 4. Invalid CUID ─────────────────────────────────────────────────────── it('returns 400 when the id path segment is not a valid CUID', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/[id]/lease/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/[id]/lease/route.test.ts index 39b9d26ce..304c70c6a 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/[id]/lease/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/[id]/lease/route.test.ts @@ -33,12 +33,6 @@ vi.mock('@/lib/db/client', () => ({ aiWorkflowExecutionLeaseEvent: { findMany: vi.fn() }, }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports (after mocks) ────────────────────────────────────────────────── @@ -46,7 +40,6 @@ vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); import { GET } from '@/app/api/v1/admin/orchestration/executions/[id]/lease/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; // ─── Constants ─────────────────────────────────────────────────────────────── @@ -133,12 +126,6 @@ describe('GET /api/v1/admin/orchestration/executions/:id/lease', () => { // ── 3. Rate limiting: 429 ──────────────────────────────────────────────── - it('returns 429 when the admin rate limiter denies the request', async () => { - vi.mocked(adminLimiter.check).mockReturnValueOnce({ success: false } as never); - const res = await GET(makeRequest(), makeContext()); - expect(res.status).toBe(429); - }); - // ── 4. Validation: 400 invalid CUID ────────────────────────────────────── it('returns 400 when the id path parameter is not a valid CUID', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/counts/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/counts/route.test.ts index 5545a8646..12adad4c3 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/counts/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/counts/route.test.ts @@ -49,13 +49,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 0 })), - }, - createRateLimitResponse: vi.fn(() => new Response(null, { status: 429 })), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -77,7 +70,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { GET } from '@/app/api/v1/admin/orchestration/executions/counts/route'; // ─── Fixtures ───────────────────────────────────────────────────────────────── @@ -105,12 +97,6 @@ beforeEach(() => { vi.clearAllMocks(); // Default: admin session, rate-limit passes, groupBy returns empty vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: 0, - }); vi.mocked(prisma.aiWorkflowExecution.groupBy).mockResolvedValue([] as never); }); @@ -145,24 +131,6 @@ describe('GET /api/v1/admin/orchestration/executions/counts', () => { // ── 3. Rate-limiting ─────────────────────────────────────────────────────── - describe('Rate limiting', () => { - it('returns 429 when the rate limiter rejects the request and does not query the database', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const response = await GET(makeRequest('?statuses=pending')); - - expect(response.status).toBe(429); - // The rate-limit branch must call the factory and NOT fall through to groupBy - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - expect(prisma.aiWorkflowExecution.groupBy).not.toHaveBeenCalled(); - }); - }); - // ── Validation failures ──────────────────────────────────────────────────── describe('Query parameter validation', () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/live/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/live/route.test.ts index a1b7b9901..31dbfbe25 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/live/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/live/route.test.ts @@ -23,13 +23,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 0 })), - }, - createRateLimitResponse: vi.fn(() => new Response(null, { status: 429 })), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -42,7 +35,6 @@ vi.mock('@/lib/orchestration/admin/live-engine-snapshot', () => ({ import { GET } from '@/app/api/v1/admin/orchestration/executions/live/route'; import { auth } from '@/lib/auth/config'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getLiveEngineSnapshot } from '@/lib/orchestration/admin/live-engine-snapshot'; import { mockAdminUser, @@ -93,12 +85,6 @@ describe('GET /api/v1/admin/orchestration/executions/live', () => { vi.clearAllMocks(); // Reset rate limiter to allow-by-default so individual tests only need // to override when testing the 429 path. - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: 0, - }); vi.mocked(getLiveEngineSnapshot).mockResolvedValue(makeSnapshot()); }); @@ -118,23 +104,6 @@ describe('GET /api/v1/admin/orchestration/executions/live', () => { expect(response.status).toBe(403); }); - it('returns 429 when the rate limiter rejects the request', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - vi.mocked(createRateLimitResponse).mockReturnValue( - new Response(null, { status: 429 }) as never - ); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(429); - }); - it('returns 200 with the snapshot wrapped in the success envelope', async () => { const snapshot = makeSnapshot(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/report-md/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/report-md/route.test.ts index 126dae79c..e65089e95 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/report-md/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/report-md/route.test.ts @@ -25,12 +25,6 @@ vi.mock('@/lib/db/client', () => ({ aiWorkflowExecution: { findUnique: vi.fn() }, }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // ─── Imports (after mocks) ────────────────────────────────────────────────── @@ -228,13 +222,6 @@ describe('GET /api/v1/admin/orchestration/executions/:id/report.md', () => { expect(res.status).toBe(200); }); - it('returns 429 when the admin rate limiter denies the request', async () => { - const { adminLimiter } = await import('@/lib/security/rate-limit'); - vi.mocked(adminLimiter.check).mockReturnValueOnce({ success: false } as never); - const res = await GET(makeRequest(), makeContext()); - expect(res.status).toBe(429); - }); - it('handles executions whose startedAt/completedAt are null (cancelled before start)', async () => { // Exercises the optional-chain `.toISOString() ?? null` branches in // the renderInfo builder. A workflow that was cancelled immediately diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/rerun/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/rerun/route.test.ts index 7f6f475dc..24395d532 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/rerun/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/rerun/route.test.ts @@ -33,12 +33,6 @@ vi.mock('@/lib/db/client', () => ({ aiWorkflowVersion: { findFirst: vi.fn() }, }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); // Stub prepareWorkflowExecution so we don't have to mount a workflow row + // version row + run the structural validator. The route is the unit under diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/review/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/review/route.test.ts index 3e7332210..05a58a48c 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/review/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/review/route.test.ts @@ -27,12 +27,6 @@ vi.mock('@/lib/db/client', () => ({ }, }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ getProvider: vi.fn(), diff --git a/tests/unit/app/api/v1/admin/orchestration/executions/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/executions/route.test.ts index 20d921916..1a13cb8aa 100644 --- a/tests/unit/app/api/v1/admin/orchestration/executions/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/executions/route.test.ts @@ -44,13 +44,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 0 })), - }, - createRateLimitResponse: vi.fn(() => new Response(null, { status: 429 })), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1') })); vi.mock('@/lib/logging', () => ({ @@ -72,7 +65,6 @@ vi.mock('@/lib/logging', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { GET } from '@/app/api/v1/admin/orchestration/executions/route'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -153,35 +145,6 @@ describe('GET /api/v1/admin/orchestration/executions', () => { }); }); - describe('Rate limiting', () => { - it('returns 429 when the rate limiter rejects the request', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 100, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - }); - - it('returns 200 when the rate limiter allows the request through', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: Date.now() + 60_000, - }); - - const response = await GET(makeRequest()); - - expect(response.status).toBe(200); - }); - }); - // ── A: all-completed page ────────────────────────────────────────────────── describe('A — all completed rows: step findMany skipped, timeInCurrentStepMs null', () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.test.ts index 860e4e912..159dce83e 100644 --- a/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/rotate-secret/route.test.ts @@ -20,13 +20,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -60,7 +53,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ // ─── Imports ──────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { prisma } from '@/lib/db/client'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { generateHookSecret } from '@/lib/orchestration/hooks/signing'; @@ -116,7 +108,6 @@ describe('POST /api/v1/admin/orchestration/hooks/:id/rotate-secret', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiEventHook.findUnique).mockResolvedValue(EXISTING_HOOK_NO_SECRET as never); vi.mocked(prisma.aiEventHook.update).mockResolvedValue({ id: VALID_ID, @@ -136,17 +127,6 @@ describe('POST /api/v1/admin/orchestration/hooks/:id/rotate-secret', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const response = await POST(makePostRequest(), makeParams(VALID_ID)); - expect(response.status).toBe(429); - }); - it('returns 400 for a non-CUID id', async () => { const response = await POST(makePostRequest(), makeParams('not-a-cuid')); expect(response.status).toBe(400); @@ -221,7 +201,6 @@ describe('DELETE /api/v1/admin/orchestration/hooks/:id/rotate-secret', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiEventHook.findUnique).mockResolvedValue(EXISTING_HOOK_WITH_SECRET as never); vi.mocked(prisma.aiEventHook.update).mockResolvedValue({ id: VALID_ID } as never); }); @@ -281,22 +260,6 @@ describe('DELETE /api/v1/admin/orchestration/hooks/:id/rotate-secret', () => { expect(logAdminAction).not.toHaveBeenCalled(); }); - it('returns 429 when rate limit exceeded', async () => { - // Arrange: rate limit exceeded - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await DELETE(makeDeleteRequest(), makeParams(VALID_ID)); - - // Assert - expect(response.status).toBe(429); - }); - it('returns 400 for a non-CUID id', async () => { // Act const response = await DELETE(makeDeleteRequest(), makeParams('not-a-cuid')); diff --git a/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/route.test.ts index b7d3dfe94..376320b3c 100644 --- a/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/hooks/[id]/route.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -92,7 +85,6 @@ vi.mock('@/lib/security/safe-url', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { validateRequestBody } from '@/lib/api/validation'; @@ -153,7 +145,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /hooks/:id', () => { @@ -289,23 +280,6 @@ describe('PATCH /hooks/:id', () => { expect(prisma.aiEventHook.update).not.toHaveBeenCalled(); }); - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await PATCH(makePatchRequest(updatePayload), makeParams(HOOK_ID)); - - // Assert - expect(response.status).toBe(429); - expect(prisma.aiEventHook.update).not.toHaveBeenCalled(); - }); - it('updates the hook and returns serialized response on success', async () => { // Arrange const existing = makeHook(); @@ -426,23 +400,6 @@ describe('DELETE /hooks/:id', () => { expect(prisma.aiEventHook.delete).not.toHaveBeenCalled(); }); - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await DELETE(makeDeleteRequest(), makeParams(HOOK_ID)); - - // Assert - expect(response.status).toBe(429); - expect(prisma.aiEventHook.delete).not.toHaveBeenCalled(); - }); - it('deletes the hook and returns { deleted: true }', async () => { // Arrange vi.mocked(prisma.aiEventHook.findUnique).mockResolvedValue(makeHook() as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.test.ts index d3f24d95a..523a01835 100644 --- a/tests/unit/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/hooks/deliveries/[id]/retry/route.test.ts @@ -21,13 +21,6 @@ vi.mock('@/lib/orchestration/hooks/registry', () => ({ retryHookDelivery: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -36,7 +29,6 @@ vi.mock('@/lib/security/ip', () => ({ import { auth } from '@/lib/auth/config'; import { retryHookDelivery } from '@/lib/orchestration/hooks/registry'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockAuthenticatedUser, @@ -69,7 +61,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('POST /hooks/deliveries/:id/retry', () => { @@ -85,15 +76,6 @@ describe('POST /hooks/deliveries/:id/retry', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await RetryDelivery(makeRequest(DELIVERY_ID), makeParams(DELIVERY_ID)); - - expect(response.status).toBe(429); - }); - it('returns 404 when delivery is not found or not retriable', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(retryHookDelivery).mockResolvedValue(false); diff --git a/tests/unit/app/api/v1/admin/orchestration/hooks/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/hooks/route.test.ts index 3416fd6fc..b6c7b96e1 100644 --- a/tests/unit/app/api/v1/admin/orchestration/hooks/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/hooks/route.test.ts @@ -34,11 +34,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/orchestration/hooks/registry', () => ({ invalidateHookCache: vi.fn(), })); @@ -52,7 +47,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { invalidateHookCache } from '@/lib/orchestration/hooks/registry'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { GET as ListHooks, POST as CreateHook } from '@/app/api/v1/admin/orchestration/hooks/route'; @@ -131,10 +125,6 @@ beforeEach(() => { // Default session: admin authenticated. Tests that need a different user override this. vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); // Reset rate limiter to allow-by-default after each test - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); - vi.mocked(createRateLimitResponse).mockReturnValue( - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ); }); describe('GET /hooks', () => { @@ -287,28 +277,6 @@ describe('POST /hooks', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited on POST', async () => { - // Arrange: rate limit exceeded - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await CreateHook( - makeCreateRequest({ - name: 'Rate Limited Hook', - eventType: 'conversation.started', - action: { type: 'webhook', url: 'https://example.com/hook' }, - }) - ); - - // Assert - expect(response.status).toBe(429); - }); - it('rejects custom action.headers that collide with reserved signing header names', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -457,23 +425,6 @@ describe('PATCH /hooks/:id', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited on PATCH', async () => { - // Arrange: rate limit exceeded - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await UpdateHook(makePatchRequest({ name: 'Updated' }), makeParams(HOOK_ID)); - - // Assert - expect(response.status).toBe(429); - }); - it('updates only eventType and passes it through to prisma.update', async () => { // Arrange: only eventType provided — all other optional fields absent vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -637,23 +588,6 @@ describe('DELETE /hooks/:id', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited on DELETE', async () => { - // Arrange: rate limit exceeded - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await DeleteHook(makeDeleteRequest(), makeParams(HOOK_ID)); - - // Assert - expect(response.status).toBe(429); - }); - it('calls invalidateHookCache after a successful delete', async () => { // Arrange: happy-path DELETE vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts index 93ad4be42..520b605a9 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts @@ -42,16 +42,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json( - { success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests.' } }, - { status: 429 } - ) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -63,7 +53,6 @@ vi.mock('@/lib/orchestration/knowledge/document-manager', () => ({ // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { confirmPreview } from '@/lib/orchestration/knowledge/document-manager'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -133,7 +122,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/confirm', () beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(confirmPreview).mockResolvedValue(makeMockDocument() as never); }); @@ -245,29 +233,6 @@ describe('POST /api/v1/admin/orchestration/knowledge/documents/:id/confirm', () // Rate limiting // --------------------------------------------------------------------------- - describe('Rate limiting', () => { - it('should return 429 when the rate limit is exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const body = { documentId: VALID_DOC_ID }; - const response = await POST(makeRequest(VALID_DOC_ID, body), makeContext(VALID_DOC_ID)); - const data = await parseResponse(response); - - expect(response.status).toBe(429); - expect(data.success).toBe(false); - expect(data.error.code).toBe('RATE_LIMIT_EXCEEDED'); - }); - - it('should not call confirmPreview when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const body = { documentId: VALID_DOC_ID }; - await POST(makeRequest(VALID_DOC_ID, body), makeContext(VALID_DOC_ID)); - - expect(confirmPreview).not.toHaveBeenCalled(); - }); - }); - // --------------------------------------------------------------------------- // Authentication // --------------------------------------------------------------------------- diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/route.test.ts index bb8e420cf..06f689d38 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/route.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -76,7 +69,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { GET, POST } from '@/app/api/v1/admin/orchestration/knowledge/documents/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { uploadDocument, uploadDocumentFromBuffer, @@ -149,7 +141,6 @@ describe('Knowledge Documents API', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── GET — List documents ──────────────────────────────────────────────── @@ -490,37 +481,4 @@ describe('Knowledge Documents API', () => { }); // ── POST — Rate limiting and auth ──────────────────────────────────────── - - describe('POST — auth and rate limiting', () => { - it('rejects unauthenticated requests', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockUnauthenticatedUser()); - - // Act - const res = await POST(makeFileRequest('test.md', '# Hello')); - - // Assert - expect(res.status).toBe(401); - }); - - it('returns 429 when rate limited', async () => { - // Arrange — fixed reset value avoids wall-clock coupling. The route - // doesn't compare reset against `Date.now()` today, but a deterministic - // value also keeps any future reset-driven assertions stable across CI - // hosts where elapsed time would drift. - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: 1_700_000_000 + 60, - } as never); - - // Act - const res = await POST(makeFileRequest('test.md', '# Hello')); - - // Assert - expect(res.status).toBe(429); - expect(uploadDocument).not.toHaveBeenCalled(); - }); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/embedding-status/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/embedding-status/route.test.ts index afeb4acde..0b64d2a7f 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/embedding-status/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/embedding-status/route.test.ts @@ -31,13 +31,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/embeddings/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/embeddings/route.test.ts index 5af6e6625..aa63aacab 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/embeddings/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/embeddings/route.test.ts @@ -37,13 +37,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -53,7 +46,6 @@ vi.mock('@/lib/security/ip', () => ({ import { GET } from '@/app/api/v1/admin/orchestration/knowledge/embeddings/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -141,7 +133,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/embeddings', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); it('returns 401 when no session is present', async () => { @@ -156,12 +147,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/embeddings', () => { expect(res.status).toBe(403); }); - it('returns 429 when the rate-limit gate fires', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ success: false, retryAfter: 30 } as never); - const res = await GET(makeGetRequest()); - expect(res.status).toBe(429); - }); - it('returns empty chunks and projectable=false when there are no embedded chunks', async () => { setupQueryRawMocks(0, []); const res = await GET(makeGetRequest()); diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/graph/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/graph/route.test.ts index 93001b205..fa2a6faf5 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/graph/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/graph/route.test.ts @@ -48,13 +48,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -64,7 +57,6 @@ vi.mock('@/lib/security/ip', () => ({ import { GET } from '@/app/api/v1/admin/orchestration/knowledge/graph/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -157,7 +149,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/graph', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser() as never); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); // Default: one document, three chunks (below 500 threshold) vi.mocked(prisma.aiKnowledgeDocument.findMany).mockResolvedValue([makeDocument()] as never); @@ -567,19 +558,6 @@ describe('GET /api/v1/admin/orchestration/knowledge/graph', () => { // ── Rate limiting ──────────────────────────────────────────────────────── - it('returns 429 when rate limit is exceeded', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const res = await GET(makeGetRequest()); - - // Assert: early return from rate limiter - expect(res.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(prisma.aiKnowledgeDocument.findMany).not.toHaveBeenCalled(); - }); - // ── Authentication ─────────────────────────────────────────────────────── it('returns 401 when unauthenticated', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/patterns/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/patterns/route.test.ts index 15246c3cc..f00bc6319 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/patterns/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/patterns/route.test.ts @@ -27,13 +27,6 @@ vi.mock('@/lib/orchestration/knowledge/search', () => ({ getPatternDetail: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/maintenance/tick/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/maintenance/tick/route.test.ts index 5998a762d..bcf7af6fb 100644 --- a/tests/unit/app/api/v1/admin/orchestration/maintenance/tick/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/maintenance/tick/route.test.ts @@ -33,13 +33,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -77,7 +70,6 @@ vi.mock('@/lib/orchestration/retention', () => ({ // ─── Imports ──────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { logger } from '@/lib/logging'; import { processDueSchedules, @@ -153,11 +145,6 @@ describe('POST /api/v1/admin/orchestration/maintenance/tick', () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); - vi.mocked(createRateLimitResponse).mockReturnValue( - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ); - vi.mocked(processDueSchedules).mockResolvedValue(DEFAULT_SCHEDULE_RESULT); vi.mocked(processPendingRetries).mockResolvedValue(DEFAULT_RETRY_RESULT); vi.mocked(processPendingHookRetries).mockResolvedValue(DEFAULT_HOOK_RETRY_RESULT); @@ -192,32 +179,6 @@ describe('POST /api/v1/admin/orchestration/maintenance/tick', () => { // ── Rate limiting ──────────────────────────────────────────────────────── - it('returns 429 when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const response = await POST(makeRequest()); - - expect(response.status).toBe(429); - }); - - it('does not run tasks when rate limited', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - await POST(makeRequest()); - - expect(processDueSchedules).not.toHaveBeenCalled(); - }); - // ── Happy path ─────────────────────────────────────────────────────────── it('returns 202 with schedules result and backgroundTasks list', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/audit/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/audit/route.test.ts index 5008e9823..8abe40c08 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/audit/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/audit/route.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -60,7 +53,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { queryMcpAuditLogs, getMcpServerConfig } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -129,7 +121,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); // Restore default rate limit behaviour after any test that overrides it - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/audit', () => { @@ -149,17 +140,6 @@ describe('GET /mcp/audit', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns paginated audit logs', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(queryMcpAuditLogs).mockResolvedValue({ @@ -227,17 +207,6 @@ describe('DELETE /mcp/audit', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('skips purge and returns deleted:0 when auditRetentionDays is 0', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(getMcpServerConfig).mockResolvedValue( diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.test.ts index 6d0cedad7..dc4cdee88 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/rotate/route.test.ts @@ -19,13 +19,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -59,7 +52,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ // ─── Imports ──────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { prisma } from '@/lib/db/client'; import { generateApiKey } from '@/lib/orchestration/mcp/auth'; import { @@ -117,7 +109,6 @@ describe('POST /api/v1/admin/orchestration/mcp/keys/:id/rotate', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.mcpApiKey.findUnique).mockResolvedValue(EXISTING_KEY as never); vi.mocked(prisma.mcpApiKey.update).mockResolvedValue(UPDATED_KEY as never); }); @@ -138,17 +129,6 @@ describe('POST /api/v1/admin/orchestration/mcp/keys/:id/rotate', () => { // ── Rate limiting ───────────────────────────────────────────────────── - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - const response = await POST(makeRequest(), { params: Promise.resolve({ id: VALID_ID }) }); - expect(response.status).toBe(429); - }); - // ── Not found ───────────────────────────────────────────────────────── it('returns 404 when key not found', async () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/route.test.ts index bd7905fca..a62ebd13c 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/[id]/route.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -64,7 +57,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -123,7 +115,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('PATCH /mcp/keys/:id', () => { @@ -143,17 +134,6 @@ describe('PATCH /mcp/keys/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ name: 'Updated' }), makeParams(KEY_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -241,17 +221,6 @@ describe('DELETE /mcp/keys/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeParams(KEY_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/route.test.ts index fa2ebb42a..62aa1aff4 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/keys/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/keys/route.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -66,7 +59,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { generateApiKey } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -122,7 +114,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/keys', () => { @@ -142,17 +133,6 @@ describe('GET /mcp/keys', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns paginated API keys', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.mcpApiKey.findMany).mockResolvedValue([makeApiKey()] as never); @@ -221,17 +201,6 @@ describe('POST /mcp/keys', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest({ name: 'Test Key', scopes: ['tools:list'] })); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('creates API key and returns plaintext once', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(generateApiKey).mockReturnValue({ diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/resources/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/resources/[id]/route.test.ts index f5973362e..0fd877cab 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/resources/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/resources/[id]/route.test.ts @@ -41,13 +41,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -65,7 +58,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { clearMcpResourceCache, broadcastMcpResourcesChanged } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -126,7 +118,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('PATCH /mcp/resources/:id', () => { @@ -146,17 +137,6 @@ describe('PATCH /mcp/resources/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ name: 'Updated' }), makeParams(RESOURCE_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -251,17 +231,6 @@ describe('DELETE /mcp/resources/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeParams(RESOURCE_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/resources/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/resources/route.test.ts index 642a58e25..06db7e44a 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/resources/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/resources/route.test.ts @@ -39,13 +39,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -63,7 +56,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { clearMcpResourceCache, broadcastMcpResourcesChanged } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -124,7 +116,6 @@ const VALID_RESOURCE_BODY = { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/resources', () => { @@ -144,17 +135,6 @@ describe('GET /mcp/resources', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns paginated resources', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.mcpExposedResource.findMany).mockResolvedValue([makeResource()] as never); @@ -210,17 +190,6 @@ describe('POST /mcp/resources', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_RESOURCE_BODY)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('creates resource and returns 201', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.mcpExposedResource.create).mockResolvedValue(makeResource() as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.test.ts index 74c5cda66..b583cb6be 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/[id]/route.test.ts @@ -25,13 +25,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -51,7 +44,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ // ─── Imports ───────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -84,7 +76,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('DELETE /mcp/sessions/:id', () => { @@ -104,17 +95,6 @@ describe('DELETE /mcp/sessions/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeParams(SESSION_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 404 when session not found', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); mockDestroySession.mockReturnValue(false); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/route.test.ts index a9239f9e3..d39473c2c 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/sessions/route.test.ts @@ -25,13 +25,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -51,7 +44,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ // ─── Imports ───────────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -86,7 +78,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/sessions', () => { @@ -106,17 +97,6 @@ describe('GET /mcp/sessions', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns list of active sessions', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); mockGetActiveSessions.mockReturnValue([makeSession(), makeSession({ id: 'session-uuid-2' })]); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/settings/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/settings/route.test.ts index 811a5cc71..72bc172fd 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/settings/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/settings/route.test.ts @@ -38,13 +38,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -67,7 +60,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getMcpServerConfig, invalidateMcpConfigCache } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -116,7 +108,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/settings', () => { @@ -136,17 +127,6 @@ describe('GET /mcp/settings', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns current MCP server config', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(getMcpServerConfig).mockResolvedValue(makeMcpConfig() as never); @@ -191,17 +171,6 @@ describe('PATCH /mcp/settings', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ isEnabled: false })); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 when no fields provided', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/tools/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/tools/[id]/route.test.ts index 5c2a4bd0f..9ca720242 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/tools/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/tools/[id]/route.test.ts @@ -41,13 +41,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -65,7 +58,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { clearMcpToolCache, broadcastMcpToolsChanged } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -137,7 +129,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('PATCH /mcp/tools/:id', () => { @@ -157,17 +148,6 @@ describe('PATCH /mcp/tools/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await PATCH(makePatchRequest({ isEnabled: false }), makeParams(TOOL_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); @@ -274,17 +254,6 @@ describe('DELETE /mcp/tools/:id', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest(), makeParams(TOOL_ID)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns 400 for invalid non-CUID id', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/mcp/tools/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/mcp/tools/route.test.ts index 267818c15..0f14b8255 100644 --- a/tests/unit/app/api/v1/admin/orchestration/mcp/tools/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/mcp/tools/route.test.ts @@ -39,13 +39,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => new Response(JSON.stringify({ error: 'rate limited' }), { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -63,7 +56,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { clearMcpToolCache, broadcastMcpToolsChanged } from '@/lib/orchestration/mcp'; import { mockAdminUser, @@ -136,7 +128,6 @@ const VALID_TOOL_BODY = { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /mcp/tools', () => { @@ -156,17 +147,6 @@ describe('GET /mcp/tools', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('returns paginated tools with capability included', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.mcpExposedTool.findMany).mockResolvedValue([makeTool()] as never); @@ -222,17 +202,6 @@ describe('POST /mcp/tools', () => { expect(response.status).toBe(403); }); - it('returns 429 when rate limit exceeded', async () => { - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(VALID_TOOL_BODY)); - - // test-review:accept no_arg_called — zero-arg side-effect trigger - expect(createRateLimitResponse).toHaveBeenCalled(); - expect(response.status).toBe(429); - }); - it('creates tool and returns 201', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); vi.mocked(prisma.mcpExposedTool.create).mockResolvedValue(makeTool() as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/observability/dashboard-stats/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/observability/dashboard-stats/route.test.ts index bad592be2..7555fa39c 100644 --- a/tests/unit/app/api/v1/admin/orchestration/observability/dashboard-stats/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/observability/dashboard-stats/route.test.ts @@ -61,18 +61,6 @@ vi.mock('@/lib/api/etag', () => ({ checkConditional: vi.fn(() => null), })); -const RATE_LIMIT_ALLOW = { success: true, limit: 100, remaining: 99, reset: 9999999999 }; -const RATE_LIMIT_DENY = { success: false, limit: 100, remaining: 0, reset: 9999999999 }; - -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 9999999999 })), - }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -83,7 +71,6 @@ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { checkConditional } from '@/lib/api/etag'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -100,10 +87,6 @@ function makeRequest(headers?: Record): NextRequest { ); } -async function parseJson(response: Response): Promise { - return JSON.parse(await response.text()) as T; -} - // ─── Default mock data ──────────────────────────────────────────────────────── function setupDefaultPrismaMocks() { @@ -122,7 +105,6 @@ describe('GET /api/v1/admin/orchestration/observability/dashboard-stats', () => beforeEach(() => { vi.clearAllMocks(); // Restore rate-limit and etag defaults - vi.mocked(adminLimiter.check).mockReturnValue(RATE_LIMIT_ALLOW); vi.mocked(checkConditional).mockReturnValue(null); setupDefaultPrismaMocks(); }); @@ -151,23 +133,6 @@ describe('GET /api/v1/admin/orchestration/observability/dashboard-stats', () => }); }); - describe('rate limiting', () => { - it('should return rate-limit response when limiter check fails', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue(RATE_LIMIT_DENY); - - // Act - const response = await GET(makeRequest()); - - // Assert - expect(response.status).toBe(429); - const body = await parseJson<{ success: boolean; error: { code: string } }>(response); - expect(body.success).toBe(false); - expect(body.error.code).toBe('RATE_LIMITED'); - }); - }); - describe('conditional GET (ETag/304)', () => { it('should return 304 Not Modified when checkConditional returns a response', async () => { // Arrange diff --git a/tests/unit/app/api/v1/admin/orchestration/providers/test-model.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/providers/test-model.route.test.ts index 6668b6371..cd8470cec 100644 --- a/tests/unit/app/api/v1/admin/orchestration/providers/test-model.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/providers/test-model.route.test.ts @@ -43,13 +43,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -62,7 +55,6 @@ vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { getProvider } from '@/lib/orchestration/llm/provider-manager'; // ─── Fixtures ───────────────────────────────────────────────────────────────── @@ -122,7 +114,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/admin/orchestration/providers/:id/test-model', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── Authentication & Authorization ───────────────────────────────────────── @@ -444,30 +435,4 @@ describe('POST /api/v1/admin/orchestration/providers/:id/test-model', () => { }); // ── Rate limiting ────────────────────────────────────────────────────────── - - describe('Rate limiting', () => { - it('returns 429 when the rate limit is exceeded', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const response = await POST(makePostRequest(), makeParams(PROVIDER_ID)); - - // Assert - expect(response.status).toBe(429); - }); - - it('does not query the database when rate-limited', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - await POST(makePostRequest(), makeParams(PROVIDER_ID)); - - // Assert: DB was not queried - expect(prisma.aiProviderConfig.findUnique).not.toHaveBeenCalled(); - }); - }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/quiz-scores/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/quiz-scores/route.test.ts index 864e7ce7b..5d065e6d5 100644 --- a/tests/unit/app/api/v1/admin/orchestration/quiz-scores/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/quiz-scores/route.test.ts @@ -32,13 +32,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/schedules/schedules.route.test.ts b/tests/unit/app/api/v1/admin/orchestration/schedules/schedules.route.test.ts index abcf31813..236ed5455 100644 --- a/tests/unit/app/api/v1/admin/orchestration/schedules/schedules.route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/schedules/schedules.route.test.ts @@ -46,13 +46,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -77,7 +70,6 @@ import { import { POST as tickScheduler } from '@/app/api/v1/admin/orchestration/schedules/tick/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { processDueSchedules } from '@/lib/orchestration/scheduling'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; @@ -142,7 +134,6 @@ describe('Schedule CRUD API', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── List Schedules ────────────────────────────────────────────────────── @@ -335,21 +326,6 @@ describe('Schedule CRUD API', () => { expect(res.status).toBe(401); }); - it('returns 429 when rate limited', async () => { - // Arrange: rate limit exceeded - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await tickScheduler(makePostRequest({})); - - expect(res.status).toBe(429); - expect(processDueSchedules).not.toHaveBeenCalled(); - }); - it('returns partial results when some schedules fail', async () => { // Arrange: 3 processed, 2 succeeded, 1 failed vi.mocked(processDueSchedules).mockResolvedValue({ diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/route.test.ts index 94bc6f220..b8841812f 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/route.test.ts @@ -40,13 +40,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -75,7 +68,6 @@ vi.mock('@/lib/api/validation', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { logAdminAction } from '@/lib/orchestration/audit/admin-audit-logger'; import { validateRequestBody } from '@/lib/api/validation'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; @@ -132,7 +124,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); describe('GET /webhooks/:id', () => { @@ -266,23 +257,6 @@ describe('PATCH /webhooks/:id', () => { expect(prisma.aiWebhookSubscription.update).not.toHaveBeenCalled(); }); - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await PATCH(makePatchRequest(updatePayload), makeParams(WEBHOOK_ID)); - - // Assert - expect(response.status).toBe(429); - expect(prisma.aiWebhookSubscription.update).not.toHaveBeenCalled(); - }); - it('updates and returns the updated webhook on success', async () => { // Arrange const existing = makeWebhook({ isActive: true }); @@ -375,23 +349,6 @@ describe('DELETE /webhooks/:id', () => { expect(prisma.aiWebhookSubscription.delete).not.toHaveBeenCalled(); }); - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await DELETE(makeDeleteRequest(), makeParams(WEBHOOK_ID)); - - // Assert - expect(response.status).toBe(429); - expect(prisma.aiWebhookSubscription.delete).not.toHaveBeenCalled(); - }); - it('deletes the webhook and returns { deleted: true }', async () => { // Arrange vi.mocked(prisma.aiWebhookSubscription.findFirst).mockResolvedValue(makeWebhook() as never); diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts index 3e6e69d2d..4ce9af33b 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts @@ -42,22 +42,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json( - { - success: false, - error: { - code: 'RATE_LIMIT_EXCEEDED', - message: 'Too many requests. Please try again later.', - }, - }, - { status: 429 } - ) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -82,7 +66,6 @@ vi.mock('@/lib/api/context', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser } from '@/tests/helpers/auth'; import { POST } from '@/app/api/v1/admin/orchestration/webhooks/[id]/test/route'; @@ -132,7 +115,6 @@ describe('POST /api/v1/admin/orchestration/webhooks/:id/test', () => { vi.clearAllMocks(); // Default: authenticated admin, rate limit passes, webhook found, fetch succeeds vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWebhookSubscription.findFirst).mockResolvedValue(makeWebhook() as never); vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 })); }); @@ -143,25 +125,6 @@ describe('POST /api/v1/admin/orchestration/webhooks/:id/test', () => { // ── Rate limiting ─────────────────────────────────────────────────────── - describe('Rate limiting', () => { - it('returns 429 and does NOT query the database when rate limit is exceeded', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 30, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const response = await POST(makeRequest(), makeParams()); - - // Assert: rate-limit short-circuits before DB - expect(response.status).toBe(429); - expect(prisma.aiWebhookSubscription.findFirst).not.toHaveBeenCalled(); - }); - }); - // ── Input validation ──────────────────────────────────────────────────── describe('Input validation', () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/retry/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/retry/route.test.ts index 14f646906..d20fc0174 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/retry/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/retry/route.test.ts @@ -29,11 +29,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/route.test.ts index e84a804f1..250a545c3 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/deliveries/route.test.ts @@ -30,11 +30,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/route.test.ts index f0a93fe4e..f4f632610 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/route.test.ts @@ -37,13 +37,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -65,7 +58,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { GET, POST } from '@/app/api/v1/admin/orchestration/webhooks/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; // ─── Fixtures ─────────────────────────────────────────────────────────────── @@ -106,7 +98,6 @@ describe('Webhook Subscription API', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); }); // ── GET — List webhooks ───────────────────────────────────────────────── @@ -330,22 +321,5 @@ describe('Webhook Subscription API', () => { // Assert expect(res.status).toBe(401); }); - - it('returns 429 when rate limited', async () => { - // Arrange - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - // Act - const res = await POST(makePostRequest(validPayload)); - - // Assert - expect(res.status).toBe(429); - expect(prisma.aiWebhookSubscription.create).not.toHaveBeenCalled(); - }); }); }); diff --git a/tests/unit/app/api/v1/admin/orchestration/webhooks/test/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/webhooks/test/route.test.ts index d3a766d77..fd82c72a2 100644 --- a/tests/unit/app/api/v1/admin/orchestration/webhooks/test/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/webhooks/test/route.test.ts @@ -25,18 +25,10 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports ──────────────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; import { POST } from '@/app/api/v1/admin/orchestration/webhooks/[id]/test/route'; @@ -74,7 +66,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWebhookSubscription.findFirst).mockResolvedValue(makeWebhook() as never); globalThis.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); }); @@ -237,21 +228,6 @@ describe('POST /webhooks/:id/test', () => { expect(body.data.error).toMatch(/timed out/i); }); - it('returns 429 when rate limit is exceeded', async () => { - // Arrange: admin limiter rejects the request - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: false } as never); - - // Act - const response = await POST(makeRequest(), makeParams()); - - // Assert: 429 with standard rate-limit error envelope - expect(response.status).toBe(429); - const body = await parseJson<{ success: boolean; error: { code: string } }>(response); - expect(body.success).toBe(false); - expect(body.error.code).toBe('RATE_LIMITED'); - }); - it('returns "Unknown error" when fetch rejects with a non-Error value', async () => { // Arrange: fetch throws a string (non-Error) — exercises the third arm of the // catch block: `err instanceof Error` is false → fallback to 'Unknown error' diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/execute/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/execute/route.test.ts index 5b46e8a87..fc8304fcd 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/execute/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/execute/route.test.ts @@ -42,18 +42,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -const RATE_LIMIT_ALLOW = { success: true, limit: 100, remaining: 99, reset: 9999999999 }; -const RATE_LIMIT_DENY = { success: false, limit: 100, remaining: 0, reset: 9999999999 }; - -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 9999999999 })), - }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -96,7 +84,6 @@ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; import { sseResponse } from '@/lib/api/sse'; import { validateWorkflow } from '@/lib/orchestration/workflows'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -185,7 +172,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/execute', () => { beforeEach(() => { vi.clearAllMocks(); // Restore safe defaults after clearAllMocks - vi.mocked(adminLimiter.check).mockReturnValue(RATE_LIMIT_ALLOW); vi.mocked(validateWorkflow).mockReturnValue({ ok: true, errors: [] }); vi.mocked(sseResponse).mockReturnValue( new Response('data: test\n\n', { @@ -219,22 +205,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/execute', () => { }); }); - describe('rate limiting', () => { - it('should return rate-limit response when limiter check fails', async () => { - // Arrange - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue(RATE_LIMIT_DENY); - - // Act - const response = await POST(makeRequest(), makeParams()); - - // Assert - expect(response.status).toBe(429); - const body = await parseJson<{ error: { code: string } }>(response); - expect(body.error.code).toBe('RATE_LIMITED'); - }); - }); - describe('workflow ID validation', () => { it('should return 400 with ValidationError for invalid (non-CUID) workflow ID', async () => { // Arrange diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts index cfb65975d..c37ce49b8 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts @@ -59,15 +59,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 9999999999 })), - }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -89,7 +80,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser, @@ -205,12 +195,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/save-as-template', () = vi.clearAllMocks(); // Restore safe defaults — adminLimiter allows by default - vi.mocked(adminLimiter.check).mockReturnValue({ - success: true, - limit: 100, - remaining: 99, - reset: 9999999999, - }); // Default: slug is unique (findUnique for slug returns null) vi.mocked(prisma.aiWorkflow.findUnique).mockImplementation((async (args: unknown) => { @@ -228,28 +212,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/save-as-template', () = // ─── Rate limiting ────────────────────────────────────────────────── - describe('rate limiting', () => { - it('should return 429 when adminLimiter.check indicates the limit is exceeded', async () => { - // Arrange: override the default (success=true) with a failed rate-limit check - vi.mocked(adminLimiter.check).mockReturnValueOnce({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60000, - }); - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - - // Act - const response = await POST(makeRequest(), makeParams()); - const body = await parseJson(response); - - // Assert: createRateLimitResponse was called and returned a 429 - expect(response.status).toBe(429); - expect(body.success).toBe(false); - expect(body.error.code).toBe('RATE_LIMITED'); - }); - }); - // ─── Authentication ───────────────────────────────────────────────── describe('authentication', () => { diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.test.ts index c78bd3881..a380e191e 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route.test.ts @@ -46,13 +46,6 @@ vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -71,7 +64,6 @@ import { } from '@/app/api/v1/admin/orchestration/workflows/[id]/schedules/[scheduleId]/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { adminLimiter } from '@/lib/security/rate-limit'; import { mockAdminUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -133,7 +125,6 @@ describe('GET /workflows/:id/schedules/:scheduleId', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowSchedule.findFirst).mockResolvedValue(mockScheduleRecord as never); }); @@ -184,21 +175,6 @@ describe('GET /workflows/:id/schedules/:scheduleId', () => { expect(json.success).toBe(false); }); - it('returns 429 when rate limit is exceeded', async () => { - // Arrange: rate limiter rejects - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await getSchedule(makeGetRequest(), makeParams(VALID_WF_ID, VALID_SCHED_ID)); - - expect(res.status).toBe(429); - expect(prisma.aiWorkflowSchedule.findFirst).not.toHaveBeenCalled(); - }); - it('returns 401 when request is unauthenticated', async () => { // Arrange: no session vi.mocked(auth.api.getSession).mockResolvedValue(mockUnauthenticatedUser()); @@ -213,7 +189,6 @@ describe('PATCH /workflows/:id/schedules/:scheduleId', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowSchedule.findFirst).mockResolvedValue(mockScheduleRecord as never); }); @@ -337,24 +312,6 @@ describe('PATCH /workflows/:id/schedules/:scheduleId', () => { expect(prisma.aiWorkflowSchedule.update).not.toHaveBeenCalled(); }); - it('returns 429 when rate limit is exceeded', async () => { - // Arrange: rate limiter rejects - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await updateSchedule( - makePatchRequest({ name: 'X' }), - makeParams(VALID_WF_ID, VALID_SCHED_ID) - ); - - expect(res.status).toBe(429); - expect(prisma.aiWorkflowSchedule.findFirst).not.toHaveBeenCalled(); - }); - it('includes inputTemplate in update data when provided', async () => { // Arrange: body includes an inputTemplate value const newTemplate = { prompt: 'run daily report' }; @@ -394,7 +351,6 @@ describe('DELETE /workflows/:id/schedules/:scheduleId', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(adminLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowSchedule.findFirst).mockResolvedValue(mockScheduleRecord as never); vi.mocked(prisma.aiWorkflowSchedule.delete).mockResolvedValue(mockScheduleRecord as never); }); @@ -454,22 +410,6 @@ describe('DELETE /workflows/:id/schedules/:scheduleId', () => { expect(prisma.aiWorkflowSchedule.delete).not.toHaveBeenCalled(); }); - it('returns 429 when rate limit is exceeded', async () => { - // Arrange: rate limiter rejects - vi.mocked(adminLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await deleteSchedule(makeDeleteRequest(), makeParams(VALID_WF_ID, VALID_SCHED_ID)); - - expect(res.status).toBe(429); - expect(prisma.aiWorkflowSchedule.findFirst).not.toHaveBeenCalled(); - expect(prisma.aiWorkflowSchedule.delete).not.toHaveBeenCalled(); - }); - it('returns 401 when request is unauthenticated', async () => { // Arrange: no session vi.mocked(auth.api.getSession).mockResolvedValue(mockUnauthenticatedUser()); diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/execute-stream/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/execute-stream/route.test.ts index c39c2ab33..9ab967057 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/execute-stream/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/execute-stream/route.test.ts @@ -32,13 +32,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/templates/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/templates/route.test.ts index 8c8f7c14e..1156734df 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/templates/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/templates/route.test.ts @@ -51,11 +51,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/orchestration/audit/admin-audit-logger', () => ({ logAdminAction: vi.fn(), })); From 587e20b4b4db577cb97d5dd31a747d32a1e5eb89 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Fri, 22 May 2026 23:46:09 +0100 Subject: [PATCH 3/6] refactor(security): drop redundant apiLimiter calls from /api/v1 handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to PR #211's middleware-driven rate limiting and to the prior commit's admin sweep. The api tier (100/min) and the new mcp tier (300/min, from commit 1 of this PR) are enforced centrally by proxy.ts via the policy table, so per-handler apiLimiter.check calls became redundant defense-in-depth. Source changes (13 handlers): - 11 codemod-applied: provider-models/route.ts (+ recommend), chat/conversations/[id]/route.ts (+ share + messages/.../rate), embed/widget-config/route.ts, orchestration/approvals/[id]/status, user/api-keys (+ [keyId]), users/me/preferences, webhooks/trigger/[slug]. - 2 manual: chat/stream/route.ts and mcp/route.ts both have per-flow sub-limiters (chatLimiter / agentChatLimiter / imageLimiter and McpRateLimiter respectively) that stay in the handler as additive caps. Only the redundant apiLimiter.check section call was removed. Special-case docstring updates: - mcp/route.ts file header: was "Rate limited at IP level via apiLimiter, then per-key via McpRateLimiter" — now describes the layered rate-limiting model (proxy applies the mcp tier; the handler's McpRateLimiter is the per-key sub-cap). - mcp/route.ts POST: removed the stale "// 1. IP-level rate limit" numbered comment that pointed at the now-removed block; replaced with a brief note explaining where rate limiting actually happens. - embed/widget-config/route.ts: stripped the now-dead `rateKey = \`${token}:${clientIp}\`` declaration that was only used to build the apiLimiter token. Test changes (14 test files, 13 deleted dead tests): - Delete describe('...rate limit...') and `it()` blocks that referenced apiLimiter in the body. Same logic + safety guard as the admin sweep in the prior commit (the blockReferencesApiLimiter check prevents false positives on tests for sub-limiters like consumerChatLimiter and McpRateLimiter, which still legitimately fire from handlers). - Delete `vi.mock('@/lib/security/rate-limit', ...)` blocks that only mock apiLimiter + createRateLimitResponse — preserve mocks that also carry sub-limiters (e.g. chat/stream/route.test.ts keeps the block because it also mocks consumerChatLimiter + agentChatLimiter). - Strip stray `vi.mocked(apiLimiter.check)` and conditionally-stray `vi.mocked(createRateLimitResponse)` setup lines (the conditional rule: only strip if the vi.mock block was also deleted, so legitimate sub-limiter test setups survive). - Clean up dead imports of apiLimiter and dead helpers (e.g. makeRateLimitResult in chat/conversations/[id]/route.test.ts). Net diff: 27 files changed, +9 / -401 lines 13 source files + 14 test files Validation: Type-check: clean Lint: clean (524 pre-existing warnings, 0 new errors) Format: clean Tests: 850/850 files passing, 17,718 tests (-13 deleted dead tests), 6 skipped, 10 todo The 13 deleted tests covered "handler calls apiLimiter.check; on failure returns 429" — moved to the middleware tests added in PR #211. Co-Authored-By: Claude Opus 4.7 --- .../provider-models/recommend/route.ts | 6 --- .../orchestration/provider-models/route.ts | 6 --- .../[id]/messages/[messageId]/rate/route.ts | 6 --- app/api/v1/chat/conversations/[id]/route.ts | 6 --- .../v1/chat/conversations/[id]/share/route.ts | 10 ---- app/api/v1/chat/stream/route.ts | 6 --- app/api/v1/embed/widget-config/route.ts | 5 -- app/api/v1/mcp/route.ts | 17 +++--- .../approvals/[id]/status/route.ts | 6 --- app/api/v1/user/api-keys/[keyId]/route.ts | 8 +-- app/api/v1/user/api-keys/route.ts | 12 +---- app/api/v1/users/me/preferences/route.ts | 10 ---- app/api/v1/webhooks/trigger/[slug]/route.ts | 4 +- .../provider-models.recommend.test.ts | 7 --- .../orchestration/provider-models.test.ts | 15 ------ .../approvals.id.approve.test.ts | 16 ------ .../orchestration/approvals.id.reject.test.ts | 19 ------- .../messages/[messageId]/rate/route.test.ts | 22 -------- .../v1/chat/conversations/[id]/route.test.ts | 44 ---------------- .../unit/app/api/v1/chat/stream/route.test.ts | 47 ----------------- .../app/api/v1/embed/widget-config.test.ts | 15 ------ tests/unit/app/api/v1/mcp/route.test.ts | 31 ----------- .../approvals/channel-routes.test.ts | 5 -- .../approvals/status/route.test.ts | 5 -- .../v1/user/api-keys/api-keys.route.test.ts | 52 ------------------- .../api/v1/users/me/preferences/route.test.ts | 14 ----- .../app/api/v1/webhooks/trigger/route.test.ts | 16 ------ 27 files changed, 9 insertions(+), 401 deletions(-) diff --git a/app/api/v1/admin/orchestration/provider-models/recommend/route.ts b/app/api/v1/admin/orchestration/provider-models/recommend/route.ts index 0e030fd08..ed2f0283e 100644 --- a/app/api/v1/admin/orchestration/provider-models/recommend/route.ts +++ b/app/api/v1/admin/orchestration/provider-models/recommend/route.ts @@ -20,18 +20,12 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { successResponse } from '@/lib/api/responses'; import { ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { recommendModels } from '@/lib/orchestration/llm/provider-selector'; import { TASK_INTENTS, type TaskIntent } from '@/types/orchestration'; const intentSchema = z.enum(TASK_INTENTS as unknown as [string, ...string[]]); export const GET = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); const rawIntent = searchParams.get('intent'); diff --git a/app/api/v1/admin/orchestration/provider-models/route.ts b/app/api/v1/admin/orchestration/provider-models/route.ts index 40989155d..655781b5f 100644 --- a/app/api/v1/admin/orchestration/provider-models/route.ts +++ b/app/api/v1/admin/orchestration/provider-models/route.ts @@ -17,8 +17,6 @@ import { paginatedResponse, successResponse } from '@/lib/api/responses'; import { ConflictError } from '@/lib/api/errors'; import { validateQueryParams, validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { invalidateModelCache } from '@/lib/orchestration/llm/provider-selector'; import { parseAudioDefault } from '@/lib/orchestration/llm/audio-default'; import { getOrchestrationSettings } from '@/lib/orchestration/settings'; @@ -29,10 +27,6 @@ import { } from '@/lib/validations/orchestration'; export const GET = withAdminAuth(async (request, _session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { searchParams } = new URL(request.url); diff --git a/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.ts b/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.ts index d2df55146..f6eae34ed 100644 --- a/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.ts +++ b/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.ts @@ -16,18 +16,12 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { validateRequestBody } from '@/lib/api/validation'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { rateMessageSchema } from '@/lib/validations/orchestration'; type Params = { id: string; messageId: string }; export const POST = withAuth(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawConvId, messageId: rawMsgId } = await params; const convId = cuidSchema.safeParse(rawConvId); diff --git a/app/api/v1/chat/conversations/[id]/route.ts b/app/api/v1/chat/conversations/[id]/route.ts index 8aae5ecde..beef46be6 100644 --- a/app/api/v1/chat/conversations/[id]/route.ts +++ b/app/api/v1/chat/conversations/[id]/route.ts @@ -16,8 +16,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; export const GET = withAuth<{ id: string }>(async (request, session, { params }) => { @@ -47,10 +45,6 @@ export const GET = withAuth<{ id: string }>(async (request, session, { params }) }); export const DELETE = withAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/chat/conversations/[id]/share/route.ts b/app/api/v1/chat/conversations/[id]/share/route.ts index 552fd4192..d9a3d2f92 100644 --- a/app/api/v1/chat/conversations/[id]/share/route.ts +++ b/app/api/v1/chat/conversations/[id]/share/route.ts @@ -30,8 +30,6 @@ import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; import { getRouteLogger } from '@/lib/api/context'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; import { shareConversationSchema } from '@/lib/validations/orchestration'; @@ -45,10 +43,6 @@ import { shareConversationSchema } from '@/lib/validations/orchestration'; const DEFAULT_EXPIRES_IN_DAYS = 7; export const POST = withAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); @@ -107,10 +101,6 @@ export const POST = withAuth<{ id: string }>(async (request, session, { params } }); export const DELETE = withAuth<{ id: string }>(async (request, session, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); diff --git a/app/api/v1/chat/stream/route.ts b/app/api/v1/chat/stream/route.ts index 8a885466e..11081a3c7 100644 --- a/app/api/v1/chat/stream/route.ts +++ b/app/api/v1/chat/stream/route.ts @@ -21,13 +21,11 @@ import { errorResponse } from '@/lib/api/responses'; import { validateRequestBody } from '@/lib/api/validation'; import { getRouteLogger } from '@/lib/api/context'; import { - apiLimiter, consumerChatLimiter, agentChatLimiter, createRateLimitResponse, imageLimiter, } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { streamChat } from '@/lib/orchestration/chat'; import { consumerChatRequestSchema } from '@/lib/validations/orchestration'; import { getRequestId } from '@/lib/logging/context'; @@ -36,10 +34,6 @@ import { NotFoundError, ForbiddenError } from '@/lib/api/errors'; import { validateImageMagicBytes, validatePdfMagicBytes } from '@/lib/storage/image'; export const POST = withAuth(async (request, session) => { - const clientIP = getClientIP(request); - const ipLimit = apiLimiter.check(clientIP); - if (!ipLimit.success) return createRateLimitResponse(ipLimit); - const userLimit = consumerChatLimiter.check(session.user.id); if (!userLimit.success) return createRateLimitResponse(userLimit); diff --git a/app/api/v1/embed/widget-config/route.ts b/app/api/v1/embed/widget-config/route.ts index 52f2c3ef7..8a2c25f9c 100644 --- a/app/api/v1/embed/widget-config/route.ts +++ b/app/api/v1/embed/widget-config/route.ts @@ -15,7 +15,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { logger } from '@/lib/logging'; import { getClientIP } from '@/lib/security/ip'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { resolveEmbedToken, isOriginAllowed } from '@/lib/embed/auth'; import { resolveWidgetConfig } from '@/lib/validations/orchestration'; import { getAudioProvider, hasModelWithCapability } from '@/lib/orchestration/llm/provider-manager'; @@ -64,10 +63,6 @@ export async function GET(request: NextRequest): Promise { const clientIp = getClientIP(request); - const rateKey = `${token}:${clientIp}`; - const rateLimit = apiLimiter.check(rateKey); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const ctx = await resolveEmbedToken(token, clientIp); if (!ctx) { return NextResponse.json( diff --git a/app/api/v1/mcp/route.ts b/app/api/v1/mcp/route.ts index 05831ded7..1c1106b8a 100644 --- a/app/api/v1/mcp/route.ts +++ b/app/api/v1/mcp/route.ts @@ -6,12 +6,14 @@ * DELETE /api/v1/mcp — Session termination * * Authentication: MCP API key (bearer token), not session cookies. - * Rate limited at IP level via apiLimiter, then per-key via McpRateLimiter. + * Rate limiting is layered: the proxy applies the section-level `mcp` tier + * (300/min keyed per api-key — see `lib/security/rate-limit-policy.ts`), + * and `McpRateLimiter` inside the handler applies the per-key sub-cap + * configured on each `apiKey.rateLimit` row. */ import { NextRequest } from 'next/server'; import { handleAPIError } from '@/lib/api/errors'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { sseResponse } from '@/lib/api/sse'; import { logger } from '@/lib/logging'; @@ -38,11 +40,8 @@ export async function POST(request: NextRequest): Promise { try { const clientIp = getClientIP(request); - // 1. IP-level rate limit - const ipLimit = apiLimiter.check(clientIp); - if (!ipLimit.success) return createRateLimitResponse(ipLimit); - - // 2. Authenticate bearer token + // Authenticate bearer token. Section-level rate limiting is enforced + // upstream by proxy.ts via the mcp tier (300/min keyed per api-key). const authHeader = request.headers.get('authorization') ?? ''; const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; const userAgent = request.headers.get('user-agent') ?? ''; @@ -279,8 +278,6 @@ export async function POST(request: NextRequest): Promise { export async function GET(request: NextRequest): Promise { try { const clientIp = getClientIP(request); - const ipLimit = apiLimiter.check(clientIp); - if (!ipLimit.success) return createRateLimitResponse(ipLimit); const authHeader = request.headers.get('authorization') ?? ''; const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; @@ -359,8 +356,6 @@ export async function GET(request: NextRequest): Promise { export async function DELETE(request: NextRequest): Promise { try { const clientIp = getClientIP(request); - const ipLimit = apiLimiter.check(clientIp); - if (!ipLimit.success) return createRateLimitResponse(ipLimit); const authHeader = request.headers.get('authorization') ?? ''; const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; diff --git a/app/api/v1/orchestration/approvals/[id]/status/route.ts b/app/api/v1/orchestration/approvals/[id]/status/route.ts index b769f63c7..eafe9ffac 100644 --- a/app/api/v1/orchestration/approvals/[id]/status/route.ts +++ b/app/api/v1/orchestration/approvals/[id]/status/route.ts @@ -23,8 +23,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { errorResponse, successResponse } from '@/lib/api/responses'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { verifyApprovalToken } from '@/lib/orchestration/approval-tokens'; import { prisma } from '@/lib/db/client'; import { cuidSchema } from '@/lib/validations/common'; @@ -45,10 +43,6 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ): Promise { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const { id: rawId } = await params; const parsed = cuidSchema.safeParse(rawId); if (!parsed.success) { diff --git a/app/api/v1/user/api-keys/[keyId]/route.ts b/app/api/v1/user/api-keys/[keyId]/route.ts index 199a4d12e..0ac466f8d 100644 --- a/app/api/v1/user/api-keys/[keyId]/route.ts +++ b/app/api/v1/user/api-keys/[keyId]/route.ts @@ -12,17 +12,11 @@ import type { AuthSession } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { NotFoundError, ValidationError } from '@/lib/api/errors'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { cuidSchema } from '@/lib/validations/common'; type Params = { keyId: string }; -export const DELETE = withAuth(async (request, session: AuthSession, { params }) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const DELETE = withAuth(async (_request, session: AuthSession, { params }) => { const { keyId: rawKeyId } = await params; const parsed = cuidSchema.safeParse(rawKeyId); if (!parsed.success) diff --git a/app/api/v1/user/api-keys/route.ts b/app/api/v1/user/api-keys/route.ts index 54001f3c6..573cc1d18 100644 --- a/app/api/v1/user/api-keys/route.ts +++ b/app/api/v1/user/api-keys/route.ts @@ -12,17 +12,11 @@ import { withAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { successResponse } from '@/lib/api/responses'; import { ForbiddenError } from '@/lib/api/errors'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import { validateRequestBody } from '@/lib/api/validation'; import { createApiKeySchema } from '@/lib/validations/orchestration'; import { generateApiKey, hashApiKey, keyPrefix } from '@/lib/auth/api-keys'; -export const GET = withAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - +export const GET = withAuth(async (_request, session) => { const keys = await prisma.aiApiKey.findMany({ where: { userId: session.user.id }, orderBy: { createdAt: 'desc' }, @@ -42,10 +36,6 @@ export const GET = withAuth(async (request, session) => { }); export const POST = withAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const body = await validateRequestBody(request, createApiKeySchema); if (body.scopes.includes('admin') && session.user.role !== 'ADMIN') { diff --git a/app/api/v1/users/me/preferences/route.ts b/app/api/v1/users/me/preferences/route.ts index 17eb1fd66..65f9338f3 100644 --- a/app/api/v1/users/me/preferences/route.ts +++ b/app/api/v1/users/me/preferences/route.ts @@ -17,8 +17,6 @@ import { validateRequestBody } from '@/lib/api/validation'; import { updatePreferencesSchema, parseUserPreferences } from '@/lib/validations/user'; import { withAuth } from '@/lib/auth/guards'; import { getRouteLogger } from '@/lib/api/context'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; import type { UserPreferences } from '@/types'; /** @@ -31,10 +29,6 @@ import type { UserPreferences } from '@/types'; * @throws UnauthorizedError if not authenticated */ export const GET = withAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); log.info('Fetching user preferences'); @@ -71,10 +65,6 @@ export const GET = withAuth(async (request, session) => { * @throws ValidationError if invalid data */ export const PATCH = withAuth(async (request, session) => { - const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); - const log = await getRouteLogger(request); log.info('Updating user preferences'); diff --git a/app/api/v1/webhooks/trigger/[slug]/route.ts b/app/api/v1/webhooks/trigger/[slug]/route.ts index 4999eb5d4..9cd3ebe14 100644 --- a/app/api/v1/webhooks/trigger/[slug]/route.ts +++ b/app/api/v1/webhooks/trigger/[slug]/route.ts @@ -20,7 +20,7 @@ import { z } from 'zod'; import { prisma } from '@/lib/db/client'; import { logger } from '@/lib/logging'; import { successResponse, errorResponse } from '@/lib/api/responses'; -import { apiLimiter, apiKeyChatLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; +import { apiKeyChatLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { getClientIP } from '@/lib/security/ip'; import { resolveApiKey, hasScope } from '@/lib/auth/api-keys'; import { slugSchema } from '@/lib/validations/common'; @@ -34,8 +34,6 @@ export async function POST( { params }: { params: Promise<{ slug: string }> } ): Promise { const clientIP = getClientIP(request); - const rateLimit = apiLimiter.check(clientIP); - if (!rateLimit.success) return createRateLimitResponse(rateLimit); // Authenticate via API key bearer token const resolved = await resolveApiKey(request); diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.recommend.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.recommend.test.ts index a380a672f..9c0e08f86 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.recommend.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.recommend.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/orchestration/llm/provider-selector', () => ({ recommendModels: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - // ─── Imports after mocks ───────────────────────────────────────────────────── import { auth } from '@/lib/auth/config'; diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts index 1b598c810..1d8bf4a99 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts @@ -77,7 +77,6 @@ vi.mock('@/lib/orchestration/settings', () => ({ import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { getOrchestrationSettings } from '@/lib/orchestration/settings'; // ─── Fixtures ──────────────────────────────────────────────────────────────── @@ -148,7 +147,6 @@ describe('GET /api/v1/admin/orchestration/provider-models', () => { // GET uses the broader apiLimiter (source route.ts:32). POST uses // adminLimiter. Mocking the wrong limiter here means a future // refactor that drops the limiter call would not be caught. - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); }); describe('Authentication & Authorization', () => { @@ -163,19 +161,6 @@ describe('GET /api/v1/admin/orchestration/provider-models', () => { const response = await GET(makeGetRequest()); expect(response.status).toBe(403); }); - - it('returns 429 when apiLimiter trips on GET', async () => { - // GET calls apiLimiter.check (source route.ts:32); a refactor - // that no-ops the limiter call would otherwise let infinite - // anonymous polling slip past. POST has its own 429 test at - // line 502; this is its GET counterpart. - vi.mocked(auth.api.getSession).mockResolvedValue(mockAdminUser()); - vi.mocked(apiLimiter.check).mockReturnValueOnce({ success: false } as never); - - const response = await GET(makeGetRequest()); - expect(response.status).toBe(429); - expect(prisma.aiProviderModel.findMany).not.toHaveBeenCalled(); - }); }); describe('List models', () => { diff --git a/tests/integration/api/v1/orchestration/approvals.id.approve.test.ts b/tests/integration/api/v1/orchestration/approvals.id.approve.test.ts index 1cafbc6d8..3e8dd116d 100644 --- a/tests/integration/api/v1/orchestration/approvals.id.approve.test.ts +++ b/tests/integration/api/v1/orchestration/approvals.id.approve.test.ts @@ -22,13 +22,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/env', () => ({ env: { BETTER_AUTH_SECRET: 'test-secret-that-is-at-least-32-characters-long', @@ -39,7 +32,6 @@ vi.mock('@/lib/env', () => ({ // ─── Imports after mocks ──────────────────────────────────────────────────── import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { generateApprovalToken } from '@/lib/orchestration/approval-tokens'; // ─── Fixtures ─────────────────────────────────────────────────────────────── @@ -96,7 +88,6 @@ async function parseJson(response: Response): Promise { describe('POST /api/v1/orchestration/approvals/:id/approve (token auth)', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 1 } as never); }); @@ -150,13 +141,6 @@ describe('POST /api/v1/orchestration/approvals/:id/approve (token auth)', () => expect(response.status).toBe(400); }); - it('returns 429 when rate limited', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - const { token } = generateApprovalToken(EXECUTION_ID, 'approve', 60); - const response = await POST(makeRequest(EXECUTION_ID, token), makeParams(EXECUTION_ID)); - expect(response.status).toBe(429); - }); - it('returns 409 when concurrent approval races (updateMany count === 0)', async () => { vi.mocked(prisma.aiWorkflowExecution.findUnique).mockResolvedValue(makeExecution() as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 0 } as never); diff --git a/tests/integration/api/v1/orchestration/approvals.id.reject.test.ts b/tests/integration/api/v1/orchestration/approvals.id.reject.test.ts index 826e71a29..f15213ba8 100644 --- a/tests/integration/api/v1/orchestration/approvals.id.reject.test.ts +++ b/tests/integration/api/v1/orchestration/approvals.id.reject.test.ts @@ -30,13 +30,6 @@ vi.mock('@/lib/db/client', () => { return { prisma }; }); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/env', () => ({ env: { BETTER_AUTH_SECRET: 'test-secret-that-is-at-least-32-characters-long', @@ -47,7 +40,6 @@ vi.mock('@/lib/env', () => ({ // ─── Imports after mocks ──────────────────────────────────────────────────── import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { generateApprovalToken } from '@/lib/orchestration/approval-tokens'; // ─── Fixtures ─────────────────────────────────────────────────────────────── @@ -99,7 +91,6 @@ function makeParams(id: string) { describe('POST /api/v1/orchestration/approvals/:id/reject (token auth)', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 1 } as never); }); @@ -173,16 +164,6 @@ describe('POST /api/v1/orchestration/approvals/:id/reject (token auth)', () => { expect(response.status).toBe(400); }); - it('returns 429 when rate limited', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - const { token } = generateApprovalToken(EXECUTION_ID, 'reject', 60); - const response = await POST( - makeRequest(EXECUTION_ID, token, { reason: 'test' }), - makeParams(EXECUTION_ID) - ); - expect(response.status).toBe(429); - }); - it('returns 409 when concurrent rejection races', async () => { vi.mocked(prisma.aiWorkflowExecution.findUnique).mockResolvedValue(makeExecution() as never); vi.mocked(prisma.aiWorkflowExecution.updateMany).mockResolvedValue({ count: 0 } as never); diff --git a/tests/unit/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.test.ts b/tests/unit/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.test.ts index 9660a3943..80599bfd0 100644 --- a/tests/unit/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.test.ts +++ b/tests/unit/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -52,7 +45,6 @@ vi.mock('@/lib/security/ip', () => ({ import { POST } from '@/app/api/v1/chat/conversations/[id]/messages/[messageId]/rate/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { mockAuthenticatedUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -79,7 +71,6 @@ describe('POST /chat/conversations/:id/messages/:messageId/rate', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAuthenticatedUser()); - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); }); it('rates an assistant message thumbs up', async () => { @@ -166,19 +157,6 @@ describe('POST /chat/conversations/:id/messages/:messageId/rate', () => { expect(res.status).toBe(400); }); - it('returns 429 when rate limited', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await POST(makeRequest({ rating: 1 }), makeParams()); - - expect(res.status).toBe(429); - }); - it('rejects unauthenticated requests (401)', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(mockUnauthenticatedUser()); diff --git a/tests/unit/app/api/v1/chat/conversations/[id]/route.test.ts b/tests/unit/app/api/v1/chat/conversations/[id]/route.test.ts index 3b1f1e210..286a4dee8 100644 --- a/tests/unit/app/api/v1/chat/conversations/[id]/route.test.ts +++ b/tests/unit/app/api/v1/chat/conversations/[id]/route.test.ts @@ -53,18 +53,6 @@ vi.mock('@/lib/auth/config', () => ({ })); // Mock rate limiters -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { - check: vi.fn(), - }, - createRateLimitResponse: vi.fn(() => - Response.json( - { success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests.' } }, - { status: 429 } - ) - ), -})); - // Mock IP utility vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), @@ -73,7 +61,6 @@ vi.mock('@/lib/security/ip', () => ({ // Import mocked modules import { prisma } from '@/lib/db/client'; import { auth } from '@/lib/auth/config'; -import { apiLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; /** * Response type interfaces @@ -122,18 +109,6 @@ async function parseResponse(response: Response): Promise { return JSON.parse(text) as T; } -/** - * Helper: build a rate-limit result - */ -function makeRateLimitResult(success: boolean) { - return { - success, - limit: 60, - remaining: success ? 59 : 0, - reset: Math.floor(Date.now() / 1000) + 3600, - }; -} - /** * Helper: create a mock auth session */ @@ -337,7 +312,6 @@ describe('DELETE /api/v1/chat/conversations/:id', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(createMockSession() as never); - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(true)); vi.mocked(prisma.aiConversation.findFirst).mockResolvedValue(makeMockConversation() as never); vi.mocked(prisma.aiConversation.delete).mockResolvedValue({} as never); }); @@ -415,24 +389,6 @@ describe('DELETE /api/v1/chat/conversations/:id', () => { // 3. Rate limiting // --------------------------------------------------------------------------- - describe('Rate limiting', () => { - it('should return 429 when the IP rate limit is exceeded', async () => { - // Arrange - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(false)); - const request = createMockRequest(); - const context = createRouteContext(VALID_CUID); - - // Act - const response = await DELETE(request, context); - - // Assert - expect(response.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - expect(prisma.aiConversation.findFirst).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - expect(prisma.aiConversation.delete).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - }); - }); - // --------------------------------------------------------------------------- // 4. Validation errors // --------------------------------------------------------------------------- diff --git a/tests/unit/app/api/v1/chat/stream/route.test.ts b/tests/unit/app/api/v1/chat/stream/route.test.ts index 43d92317e..1a41aeed2 100644 --- a/tests/unit/app/api/v1/chat/stream/route.test.ts +++ b/tests/unit/app/api/v1/chat/stream/route.test.ts @@ -96,7 +96,6 @@ vi.mock('@/lib/logging/context', () => ({ import { prisma } from '@/lib/db/client'; import { auth } from '@/lib/auth/config'; import { - apiLimiter, consumerChatLimiter, agentChatLimiter, createRateLimitResponse, @@ -204,7 +203,6 @@ describe('POST /api/v1/chat/stream', () => { vi.mocked(auth.api.getSession).mockResolvedValue(createMockSession() as never); // Default: rate limits allow the request - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(true)); vi.mocked(consumerChatLimiter.check).mockReturnValue(makeRateLimitResult(true)); // Default: public agent found @@ -347,51 +345,6 @@ describe('POST /api/v1/chat/stream', () => { // 3. Rate limiting // --------------------------------------------------------------------------- - describe('Rate limiting', () => { - it('should return 429 when the IP rate limit is exceeded', async () => { - // Arrange - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(false, 0)); - const request = createMockRequest(validPayload); - - // Act - const response = await POST(request); - - // Assert - expect(response.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - expect(streamChat).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - }); - - it('should return 429 when the per-user chat rate limit is exceeded', async () => { - // Arrange: IP passes but user limit fails - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(true)); - vi.mocked(consumerChatLimiter.check).mockReturnValue(makeRateLimitResult(false, 0)); - const request = createMockRequest(validPayload); - - // Act - const response = await POST(request); - - // Assert - expect(response.status).toBe(429); - expect(createRateLimitResponse).toHaveBeenCalledOnce(); - expect(streamChat).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - }); - - it('should check the IP limiter before the user limiter', async () => { - // Arrange: both would fail, but IP is checked first - vi.mocked(apiLimiter.check).mockReturnValue(makeRateLimitResult(false, 0)); - vi.mocked(consumerChatLimiter.check).mockReturnValue(makeRateLimitResult(false, 0)); - const request = createMockRequest(validPayload); - - // Act - await POST(request); - - // Assert: IP limiter called; user limiter never reached - expect(apiLimiter.check).toHaveBeenCalledOnce(); - expect(consumerChatLimiter.check).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - }); - }); - // --------------------------------------------------------------------------- // 4. Validation errors // --------------------------------------------------------------------------- diff --git a/tests/unit/app/api/v1/embed/widget-config.test.ts b/tests/unit/app/api/v1/embed/widget-config.test.ts index f2ef32137..903077c23 100644 --- a/tests/unit/app/api/v1/embed/widget-config.test.ts +++ b/tests/unit/app/api/v1/embed/widget-config.test.ts @@ -36,13 +36,6 @@ vi.mock('@/lib/orchestration/llm/provider-manager', () => ({ hasModelWithCapability: vi.fn(), })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -53,7 +46,6 @@ vi.mock('@/lib/logging', () => ({ import { resolveEmbedToken, isOriginAllowed } from '@/lib/embed/auth'; import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { getAudioProvider, hasModelWithCapability } from '@/lib/orchestration/llm/provider-manager'; import { GET, OPTIONS } from '@/app/api/v1/embed/widget-config/route'; import { DEFAULT_WIDGET_CONFIG } from '@/lib/validations/orchestration'; @@ -88,7 +80,6 @@ async function parseJson(response: Response): Promise { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(resolveEmbedToken).mockResolvedValue(VALID_CONTEXT as never); vi.mocked(isOriginAllowed).mockReturnValue(true); // `vi.clearAllMocks()` wipes any mock implementation set in `vi.mock()` @@ -138,12 +129,6 @@ describe('GET /api/v1/embed/widget-config', () => { expect(body.error.code).toBe('ORIGIN_DENIED'); }); - it('returns 429 when rate-limited', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - const response = await GET(makeGetRequest({ 'X-Embed-Token': VALID_TOKEN })); - expect(response.status).toBe(429); - }); - it('returns DEFAULT_WIDGET_CONFIG when stored widgetConfig is null', async () => { vi.mocked(prisma.aiAgent.findUnique).mockResolvedValue({ widgetConfig: null } as never); const response = await GET(makeGetRequest({ 'X-Embed-Token': VALID_TOKEN })); diff --git a/tests/unit/app/api/v1/mcp/route.test.ts b/tests/unit/app/api/v1/mcp/route.test.ts index 5ea4866c8..d88d606e3 100644 --- a/tests/unit/app/api/v1/mcp/route.test.ts +++ b/tests/unit/app/api/v1/mcp/route.test.ts @@ -54,11 +54,6 @@ const mockRateLimiter = { check: vi.fn(() => ({ success: true, remaining: 59 })), }; -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => new Response('Rate limited', { status: 429 })), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -99,7 +94,6 @@ vi.mock('@/lib/orchestration/mcp', () => ({ // ─── Imports ──────────────────────────────────────────────────────────── -import { apiLimiter } from '@/lib/security/rate-limit'; import { authenticateMcpRequest, getMcpServerConfig, @@ -162,7 +156,6 @@ beforeEach(() => { capturedIterable = null; // Restore default mock behaviours - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); vi.mocked(authenticateMcpRequest).mockResolvedValue(mockAuthContext); vi.mocked(getMcpServerConfig).mockResolvedValue(mockServerState as never); vi.mocked(handleMcpRequest).mockResolvedValue({ @@ -181,14 +174,6 @@ beforeEach(() => { // ───────────────────────────────────────────────────────────────────────────── describe('POST /mcp', () => { - it('returns 429 when IP rate limit exceeded', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - - const response = await POST(makePostRequest(makeRpcRequest('tools/list'))); - - expect(response.status).toBe(429); - }); - it('returns 401 JSON-RPC error when authentication fails', async () => { vi.mocked(authenticateMcpRequest).mockResolvedValue(null); @@ -488,14 +473,6 @@ describe('POST /mcp', () => { // ───────────────────────────────────────────────────────────────────────────── describe('GET /mcp', () => { - it('returns 429 when IP rate limit exceeded', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - - const response = await GET(makeGetRequest()); - - expect(response.status).toBe(429); - }); - it('returns 401 JSON-RPC error when authentication fails', async () => { vi.mocked(authenticateMcpRequest).mockResolvedValue(null); @@ -672,14 +649,6 @@ describe('GET /mcp', () => { // ───────────────────────────────────────────────────────────────────────────── describe('DELETE /mcp', () => { - it('returns 429 when IP rate limit exceeded', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ success: false } as never); - - const response = await DELETE(makeDeleteRequest()); - - expect(response.status).toBe(429); - }); - it('returns 401 JSON-RPC error when authentication fails', async () => { vi.mocked(authenticateMcpRequest).mockResolvedValue(null); diff --git a/tests/unit/app/api/v1/orchestration/approvals/channel-routes.test.ts b/tests/unit/app/api/v1/orchestration/approvals/channel-routes.test.ts index 8465d0ac1..173377f29 100644 --- a/tests/unit/app/api/v1/orchestration/approvals/channel-routes.test.ts +++ b/tests/unit/app/api/v1/orchestration/approvals/channel-routes.test.ts @@ -17,11 +17,6 @@ import { NextRequest } from 'next/server'; // resolved to at module load time in the test runtime). const APP_URL = 'http://localhost:3000'; -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/orchestration/approvals/status/route.test.ts b/tests/unit/app/api/v1/orchestration/approvals/status/route.test.ts index fbc796796..c5f3c817e 100644 --- a/tests/unit/app/api/v1/orchestration/approvals/status/route.test.ts +++ b/tests/unit/app/api/v1/orchestration/approvals/status/route.test.ts @@ -18,11 +18,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/user/api-keys/api-keys.route.test.ts b/tests/unit/app/api/v1/user/api-keys/api-keys.route.test.ts index 0e4ab2ae5..97a2a11e4 100644 --- a/tests/unit/app/api/v1/user/api-keys/api-keys.route.test.ts +++ b/tests/unit/app/api/v1/user/api-keys/api-keys.route.test.ts @@ -32,13 +32,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); @@ -61,7 +54,6 @@ import { GET, POST } from '@/app/api/v1/user/api-keys/route'; import { DELETE } from '@/app/api/v1/user/api-keys/[keyId]/route'; import { auth } from '@/lib/auth/config'; import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { mockAuthenticatedUser, mockUnauthenticatedUser } from '@/tests/helpers/auth'; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -99,7 +91,6 @@ describe('API Key Endpoints', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(auth.api.getSession).mockResolvedValue(mockAuthenticatedUser()); - vi.mocked(apiLimiter.check).mockReturnValue({ success: true } as never); }); // ── GET — List keys ──────────────────────────────────────────────────── @@ -147,21 +138,6 @@ describe('API Key Endpoints', () => { expect(res.status).toBe(401); }); - - it('returns 429 when rate limited on GET', async () => { - // Arrange: rate limit exceeded - vi.mocked(apiLimiter.check).mockReturnValue({ - success: false, - limit: 20, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await GET(makeGetRequest()); - - expect(res.status).toBe(429); - expect(prisma.aiApiKey.findMany).not.toHaveBeenCalled(); - }); }); // ── POST — Create key ────────────────────────────────────────────────── @@ -269,21 +245,6 @@ describe('API Key Endpoints', () => { }) ); }); - - it('returns 429 when rate limited on POST', async () => { - // Arrange: rate limit exceeded - vi.mocked(apiLimiter.check).mockReturnValue({ - success: false, - limit: 20, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await POST(makePostRequest({ name: 'My Key', scopes: ['chat'] })); - - expect(res.status).toBe(429); - expect(prisma.aiApiKey.create).not.toHaveBeenCalled(); - }); }); // ── DELETE — Revoke key ──────────────────────────────────────────────── @@ -326,18 +287,5 @@ describe('API Key Endpoints', () => { expect(res.status).toBe(404); }); - - it('returns 429 when rate limited', async () => { - vi.mocked(apiLimiter.check).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - } as never); - - const res = await DELETE(makeDeleteRequest(), makeKeyParams()); - - expect(res.status).toBe(429); - }); }); }); diff --git a/tests/unit/app/api/v1/users/me/preferences/route.test.ts b/tests/unit/app/api/v1/users/me/preferences/route.test.ts index f087c8266..208025a01 100644 --- a/tests/unit/app/api/v1/users/me/preferences/route.test.ts +++ b/tests/unit/app/api/v1/users/me/preferences/route.test.ts @@ -39,20 +39,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn( - () => - new Response( - JSON.stringify({ - success: false, - error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests.' }, - }), - { status: 429 } - ) - ), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/webhooks/trigger/route.test.ts b/tests/unit/app/api/v1/webhooks/trigger/route.test.ts index 40518154c..37e417731 100644 --- a/tests/unit/app/api/v1/webhooks/trigger/route.test.ts +++ b/tests/unit/app/api/v1/webhooks/trigger/route.test.ts @@ -194,22 +194,6 @@ describe('POST /api/v1/webhooks/trigger/:slug', () => { }); }); - it('returns 429 when rate limited', async () => { - (apiLimiter.check as ReturnType).mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: Date.now() + 60_000, - }); - - const res = await POST(makeRequest({}), { - params: Promise.resolve({ slug: 'my-workflow' }), - }); - - expect(res.status).toBe(429); - expect(prisma.aiWorkflow.findFirst).not.toHaveBeenCalled(); // test-review:accept no_arg_called — error-path guard: function must not be called; - }); - it('returns 500 when execution creation fails', async () => { (prisma.aiWorkflow.findFirst as ReturnType).mockResolvedValue(mockWorkflow); (prisma.aiWorkflowExecution.create as ReturnType).mockRejectedValue( From d190ccf998cac104f5c33073c80e8eb3dc15f8ad Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Fri, 22 May 2026 23:56:14 +0100 Subject: [PATCH 4/6] chore(security): clean up stale references to {admin,api}Limiter in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final pass on PR scope — sweeps the leftover doc/mock-factory references that the prior commits' codemods couldn't reach (because the surrounding code was load-bearing for unrelated reasons). All purely cosmetic / hygiene; no behaviour changes. Stale docstrings updated (~16 files): - Test-file JSDoc lines like `* - Rate limited (adminLimiter)` referenced per-handler limiter calls that no longer exist. Updated to point at the middleware tier instead: `* - Rate limiting enforced by proxy.ts (orchestration tier)`. Same treatment for the per-method variants ("on POST", "on PATCH and DELETE") and the inline comment in `models.test.ts` about the `?refresh=true` refresh path. - One outdated note in conversations.id.get.test.ts ("No rate-limiting call on GET (only DELETE has adminLimiter)") deleted entirely — neither method calls adminLimiter now. - Two integration-test docstrings in chat.stream.test.ts and the admin chat/stream route.test.ts that listed adminLimiter as a key security assertion: rewritten to enumerate the per-flow caps that do still hold (chatLimiter, agentChatLimiter, imageLimiter) and point at proxy.ts for the section tier. Dead mock-factory entries removed (~6 files): - `vi.mock('@/lib/security/rate-limit', () => ({ adminLimiter: ..., ... }))` blocks where the `adminLimiter:` / `apiLimiter:` entry was the only thing the prior codemod hadn't touched because the surrounding mock block had a sub-limiter that kept it alive. The dead entry itself contributed nothing (source doesn't import either symbol any more); removing it doesn't change test behaviour but matches the actual surface of the rate-limit module. - `background-execution-crash-flow.test.ts`: the entire `vi.mock('@/lib/security/rate-limit')` block was dead (only mocked adminLimiter + createRateLimitResponse) — deleted. - `webhooks/trigger/route.test.ts`: stripped the dead `apiLimiter:` mock entry, the now-unused `import { apiLimiter }` line, and a `(apiLimiter.check as ReturnType).mockReturnValue(...)` beforeEach setup (a type-cast pattern the earlier `vi.mocked(...)` regex didn't catch). - `chat/stream` and `chat.stream` (admin variant) tests: stripped apiLimiter/adminLimiter from their `vi.mock` factory bodies. - `provider-models.test.ts` (integration): the entire rate-limit vi.mock block was made of dead entries — removed; the inline comment about "GET uses apiLimiter, POST uses adminLimiter" was stale too, deleted. Net diff: 30 files changed, +30 / -55 lines Validation: Type-check, lint, format: clean Tests: 850/850 files passing, 17,718 tests, 6 skipped, 10 todo Co-Authored-By: Claude Opus 4.7 --- .../agents.id.capabilities.usage.test.ts | 2 +- .../v1/admin/orchestration/agents.id.clone.test.ts | 2 +- .../api/v1/admin/orchestration/chat.stream.test.ts | 5 +++-- .../admin/orchestration/conversations.clear.test.ts | 2 +- .../admin/orchestration/conversations.id.get.test.ts | 1 - .../v1/admin/orchestration/conversations.id.test.ts | 2 +- .../v1/admin/orchestration/embedding-models.test.ts | 2 +- .../orchestration/evaluations.id.complete.test.ts | 2 +- .../api/v1/admin/orchestration/evaluations.test.ts | 2 +- .../orchestration/knowledge-enrich-keywords.test.ts | 2 +- .../knowledge.documents.id.rechunk.test.ts | 2 +- .../knowledge.documents.id.retry.test.ts | 2 +- .../orchestration/knowledge.documents.id.test.ts | 2 +- .../admin/orchestration/knowledge.documents.test.ts | 2 +- .../v1/admin/orchestration/knowledge.embed.test.ts | 2 +- .../orchestration/knowledge.embedding-status.test.ts | 2 +- .../v1/admin/orchestration/knowledge.graph.test.ts | 2 +- .../v1/admin/orchestration/knowledge.search.test.ts | 2 +- .../v1/admin/orchestration/knowledge.seed.test.ts | 2 +- .../api/v1/admin/orchestration/models.test.ts | 2 +- .../v1/admin/orchestration/provider-models.test.ts | 11 ----------- .../api/v1/admin/orchestration/webhooks.id.test.ts | 2 +- .../orchestration/webhooks/[id]/test/route.test.ts | 2 +- .../background-execution-crash-flow.test.ts | 5 ----- .../v1/admin/orchestration/chat/stream/route.test.ts | 12 ++++++------ .../knowledge/documents/[id]/confirm/route.test.ts | 2 +- .../workflows/[id]/save-as-template/route.test.ts | 2 -- .../v1/chat/stream/attachments.regression.test.ts | 1 - tests/unit/app/api/v1/chat/stream/route.test.ts | 3 --- tests/unit/app/api/v1/webhooks/trigger/route.test.ts | 3 --- 30 files changed, 30 insertions(+), 55 deletions(-) diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts index 7e86984b3..d9caa0366 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.capabilities.usage.test.ts @@ -7,7 +7,7 @@ * * Key assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Invalid CUID returns 400 * - Returns usage map keyed by capability slug for the last 60 seconds * - Rows without a slug are excluded from the usage map diff --git a/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts b/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts index b862ccd53..cf300df41 100644 --- a/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts +++ b/tests/integration/api/v1/admin/orchestration/agents.id.clone.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - 201 on success: cloned agent returned * - 404 when source agent not found * - 400 when CUID is invalid diff --git a/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts b/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts index 9f903e7d1..7beed3ec3 100644 --- a/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts +++ b/tests/integration/api/v1/admin/orchestration/chat.stream.test.ts @@ -7,7 +7,9 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting is enforced by the proxy (orchestration tier — see + * `lib/security/rate-limit-policy.ts`); per-flow caps applied by + * chatLimiter / agentChatLimiter inside the handler still hold. * - SSE bridge never leaks raw error messages to the client * - If the upstream streamChat iterable throws with a secret value, * the wire output contains only the sanitized terminal frame. @@ -37,7 +39,6 @@ vi.mock('@/lib/orchestration/chat', () => ({ })); vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, chatLimiter: { check: vi.fn(() => ({ success: true })) }, agentChatLimiter: { check: vi.fn(() => ({ success: true })) }, imageLimiter: { check: vi.fn(() => ({ success: true })) }, diff --git a/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts index e3797dee3..3286b09eb 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.clear.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Empty body rejected by Zod refine (at least one of olderThan/agentId required) — * CRITICAL safety net preventing accidental "delete all conversations" calls. * - Default scope is `userId: session.user.id` (caller's own conversations). diff --git a/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts index 16c146bc9..63c61fc80 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.id.get.test.ts @@ -10,7 +10,6 @@ * - Returns conversation with agent and _count includes * - Cross-user access returns 404 (NOT 403) * - Bad CUID returns 400 - * - No rate-limiting call on GET (only DELETE has adminLimiter) */ import { describe, it, expect, beforeEach, vi } from 'vitest'; diff --git a/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts b/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts index 91c8c2c41..ad25888e8 100644 --- a/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/conversations.id.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Ownership enforced via findFirst({ where: { id, userId } }) * - Cross-user access returns 404 (NOT 403) * - AiMessage rows cascade via Prisma FK — aiMessage.deleteMany is NOT diff --git a/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts b/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts index a76974713..7a6c92af3 100644 --- a/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/embedding-models.test.ts @@ -8,7 +8,7 @@ * * Key assertions: * - Admin auth required (401 unauthenticated, 403 non-admin) - * - Rate limited via adminLimiter + * - Rate limiting enforced by proxy.ts (orchestration tier) * - No params → all 9 models returned * - schemaCompatibleOnly=true → only schema-compatible models * - hasFreeTier=true → only free-tier models diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts index 1088134c9..190383b73 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.id.complete.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - 200 on success: response body contains { session: CompleteEvaluationResult } * - 404 when handler throws NotFoundError * - 409 when handler throws ConflictError diff --git a/tests/integration/api/v1/admin/orchestration/evaluations.test.ts b/tests/integration/api/v1/admin/orchestration/evaluations.test.ts index 9eb77cc25..b05c6f3ba 100644 --- a/tests/integration/api/v1/admin/orchestration/evaluations.test.ts +++ b/tests/integration/api/v1/admin/orchestration/evaluations.test.ts @@ -10,7 +10,7 @@ * - Admin auth required (401/403 otherwise) * - GET: results ALWAYS scoped to session.user.id * - GET: optional filters (agentId, status, q) applied alongside userId scope - * - POST: rate limited (adminLimiter) + * - POST: rate limiting enforced by proxy.ts (orchestration tier) * - POST: 404 when agent doesn't exist (agents are shared admin-wide) * - POST: 400 on invalid body (missing title) * - POST: 201 on success diff --git a/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts index 703d67296..67a520724 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge-enrich-keywords.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Bad CUID returns 400 * - Missing document returns 404 * - Document with status=processing returns 409 (race condition guard) diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts index 61739a0b1..4bb7da90b 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.rechunk.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Bad CUID returns 400 * - Missing document returns 404 * - Document with status=processing returns 409 (race condition guard) diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts index 3c58c5f6e..f91ba2a2e 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.retry.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Bad CUID returns 400 * - Missing document returns 404 * - Document not in "failed" state returns 409 (ConflictError) diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts index 8adee4932..1e08f9e94 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.id.test.ts @@ -8,7 +8,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited on DELETE (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Bad CUID returns 400 * - Missing document returns 404 * diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts index b794572ae..fb43208ce 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.documents.test.ts @@ -8,7 +8,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited on POST (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - File size limit enforced (50 MB; 413 FILE_TOO_LARGE) * - Extension whitelist enforced via `ALLOWED_EXTENSIONS` (400 INVALID_FILE_TYPE) * - Pre-parse Content-Length guard rejects oversize bodies before allocation diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts index 99bf4f195..406595f00 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.embed.test.ts @@ -7,7 +7,7 @@ * * Key assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - embedChunks is called once on success * - Response data matches embedChunks return value */ diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts index 7d7347b3e..222dca86a 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.embedding-status.test.ts @@ -7,7 +7,7 @@ * * Key assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Returns correct counts: total, embedded, pending * - hasActiveProvider: true when active aiProviderConfig row exists * - hasActiveProvider: true via OPENAI_API_KEY env fallback diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts index 640f639ff..f79710d24 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.graph.test.ts @@ -7,7 +7,7 @@ * * Key assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Returns nodes, links, categories, and stats * - Scopes filter to "system" | "app" when provided * - view=embedded uses $queryRaw for embedded-only chunk aggregation diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts index 753cd13f9..10ca889f5 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.search.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Missing required query field returns 400 */ diff --git a/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts b/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts index f9f4f38a4..0e9265987 100644 --- a/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts +++ b/tests/integration/api/v1/admin/orchestration/knowledge.seed.test.ts @@ -7,7 +7,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - seedChunks is called with path ending in prisma/seeds/data/chunks/chunks.json * - Response contains { seeded: true } */ diff --git a/tests/integration/api/v1/admin/orchestration/models.test.ts b/tests/integration/api/v1/admin/orchestration/models.test.ts index 50197e4ad..772209009 100644 --- a/tests/integration/api/v1/admin/orchestration/models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/models.test.ts @@ -10,7 +10,7 @@ * warm call may not reach the same module instance) * - ?refresh=true calls refreshFromOpenRouter({ force: true }) * - ?refresh=true response includes refreshed: true - * - ?refresh=true is rate-limited (adminLimiter.check) + * - ?refresh=true takes the orchestration tier cap from proxy.ts * * @see app/api/v1/admin/orchestration/models/route.ts */ diff --git a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts index 1d8bf4a99..060d7533d 100644 --- a/tests/integration/api/v1/admin/orchestration/provider-models.test.ts +++ b/tests/integration/api/v1/admin/orchestration/provider-models.test.ts @@ -50,14 +50,6 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - apiLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(() => - Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) - ), -})); - vi.mock('@/lib/orchestration/llm/provider-selector', () => ({ invalidateModelCache: vi.fn(), })); @@ -144,9 +136,6 @@ async function parseJson(response: Response): Promise { describe('GET /api/v1/admin/orchestration/provider-models', () => { beforeEach(() => { vi.clearAllMocks(); - // GET uses the broader apiLimiter (source route.ts:32). POST uses - // adminLimiter. Mocking the wrong limiter here means a future - // refactor that drops the limiter call would not be caught. }); describe('Authentication & Authorization', () => { diff --git a/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts b/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts index a13036c17..68b372dc3 100644 --- a/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts +++ b/tests/integration/api/v1/admin/orchestration/webhooks.id.test.ts @@ -9,7 +9,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited on PATCH and DELETE (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Scoped to calling user's own subscriptions (createdBy) * - Bad CUID returns 400 * - Missing or foreign webhook returns 404 diff --git a/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts b/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts index 3821c561b..ef90f0d70 100644 --- a/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts +++ b/tests/integration/api/v1/admin/orchestration/webhooks/[id]/test/route.test.ts @@ -5,7 +5,7 @@ * * Key security assertions: * - Admin auth required (401/403 otherwise) - * - Rate limited (adminLimiter) + * - Rate limiting enforced by proxy.ts (orchestration tier) * - Scoped to calling user's own webhooks (createdBy filter) * - Bad CUID returns 400 * - Missing signing secret returns 200 with a descriptive error payload diff --git a/tests/integration/orchestration/background-execution-crash-flow.test.ts b/tests/integration/orchestration/background-execution-crash-flow.test.ts index b1531b0b5..43a08a058 100644 --- a/tests/integration/orchestration/background-execution-crash-flow.test.ts +++ b/tests/integration/orchestration/background-execution-crash-flow.test.ts @@ -32,11 +32,6 @@ vi.mock('next/headers', () => ({ headers: vi.fn(() => Promise.resolve(new Headers())), })); -vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { check: vi.fn(() => ({ success: true })) }, - createRateLimitResponse: vi.fn(), -})); - vi.mock('@/lib/security/ip', () => ({ getClientIP: vi.fn(() => '127.0.0.1'), })); diff --git a/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts index 49d7d9fd0..0020efd30 100644 --- a/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/chat/stream/route.test.ts @@ -1,13 +1,16 @@ /** * Unit Tests: POST /api/v1/admin/orchestration/chat/stream * - * Tests the admin-facing SSE chat endpoint. Uses withAdminAuth guard - * and two rate limiters (adminLimiter per IP, chatLimiter per user). + * Tests the admin-facing SSE chat endpoint. Uses withAdminAuth guard and + * three per-flow rate limiters in the handler (chatLimiter per user, + * agentChatLimiter per agent, imageLimiter for attachment-bearing requests). + * Section-level rate limiting (orchestration tier, 120/min) is enforced + * centrally by proxy.ts via the policy table. * * Test Coverage: * - Happy path: valid request → calls streamChat with correct args → SSE response - * - Rate limit exceeded (admin IP limiter) → 429 * - Rate limit exceeded (chat user limiter) → 429 + * - Rate limit exceeded (per-agent limiter) → 429 * - Invalid body: missing required fields → 400 VALIDATION_ERROR * - Attachments forwarded to streamChat * - Authentication: no session → 401 (delegated to withAdminAuth) @@ -40,9 +43,6 @@ vi.mock('@/lib/auth/config', () => ({ // Mock rate limiters vi.mock('@/lib/security/rate-limit', () => ({ - adminLimiter: { - check: vi.fn(), - }, chatLimiter: { check: vi.fn(), }, diff --git a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts index 520b605a9..b6e2adc70 100644 --- a/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/knowledge/documents/[id]/confirm/route.test.ts @@ -15,7 +15,7 @@ * * Key Behaviors: * - withAdminAuth wraps the handler; auth is mocked via auth.api.getSession - * - adminLimiter.check is called before business logic + * - Rate limiting enforced by proxy.ts (orchestration tier) * - cuidSchema validates the URL :id param before confirmPreview is called * - body.documentId must equal the URL :id param * - confirmPreview receives (id, session.user.id, correctedContent, category) diff --git a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts index c37ce49b8..c932653f0 100644 --- a/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/workflows/[id]/save-as-template/route.test.ts @@ -194,8 +194,6 @@ describe('POST /api/v1/admin/orchestration/workflows/:id/save-as-template', () = beforeEach(() => { vi.clearAllMocks(); - // Restore safe defaults — adminLimiter allows by default - // Default: slug is unique (findUnique for slug returns null) vi.mocked(prisma.aiWorkflow.findUnique).mockImplementation((async (args: unknown) => { const { where } = args as { where: { id?: string; slug?: string } }; diff --git a/tests/unit/app/api/v1/chat/stream/attachments.regression.test.ts b/tests/unit/app/api/v1/chat/stream/attachments.regression.test.ts index c14d93a74..f9bb3f0dc 100644 --- a/tests/unit/app/api/v1/chat/stream/attachments.regression.test.ts +++ b/tests/unit/app/api/v1/chat/stream/attachments.regression.test.ts @@ -33,7 +33,6 @@ vi.mock('@/lib/auth/config', () => ({ })); vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true, limit: 100, remaining: 99, reset: 0 })) }, consumerChatLimiter: { check: vi.fn(() => ({ success: true, limit: 20, remaining: 19, reset: 0 })), }, diff --git a/tests/unit/app/api/v1/chat/stream/route.test.ts b/tests/unit/app/api/v1/chat/stream/route.test.ts index 1a41aeed2..399e07328 100644 --- a/tests/unit/app/api/v1/chat/stream/route.test.ts +++ b/tests/unit/app/api/v1/chat/stream/route.test.ts @@ -54,9 +54,6 @@ vi.mock('@/lib/auth/config', () => ({ // Mock rate limiters vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { - check: vi.fn(), - }, consumerChatLimiter: { check: vi.fn(), }, diff --git a/tests/unit/app/api/v1/webhooks/trigger/route.test.ts b/tests/unit/app/api/v1/webhooks/trigger/route.test.ts index 37e417731..e1b12e567 100644 --- a/tests/unit/app/api/v1/webhooks/trigger/route.test.ts +++ b/tests/unit/app/api/v1/webhooks/trigger/route.test.ts @@ -41,7 +41,6 @@ vi.mock('@/lib/logging', () => ({ })); vi.mock('@/lib/security/rate-limit', () => ({ - apiLimiter: { check: vi.fn(() => ({ success: true })) }, apiKeyChatLimiter: { check: vi.fn(() => ({ success: true })), reset: vi.fn() }, createRateLimitResponse: vi.fn(() => Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) @@ -61,7 +60,6 @@ vi.mock('@/lib/auth/api-keys', () => ({ import { POST } from '@/app/api/v1/webhooks/trigger/[slug]/route'; import { prisma } from '@/lib/db/client'; -import { apiLimiter } from '@/lib/security/rate-limit'; import { resolveApiKey, hasScope } from '@/lib/auth/api-keys'; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -104,7 +102,6 @@ const mockWorkflow = { describe('POST /api/v1/webhooks/trigger/:slug', () => { beforeEach(() => { vi.clearAllMocks(); - (apiLimiter.check as ReturnType).mockReturnValue({ success: true }); // Default: valid API key with webhook scope vi.mocked(resolveApiKey).mockResolvedValue({ session: { user: { id: 'u1' } } as never, From b49e817478c8d76542e7f73e5025ed4cfd1d1dd3 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Sat, 23 May 2026 00:30:10 +0100 Subject: [PATCH 5/6] test(security): refresh stale 'api-key' comment in rate-limit middleware test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses finding #2 from .reviews/tests-lib-security.md (alignment, 88 confidence). The comment introducing the synthetic-rule injection at rate-limit-middleware.test.ts:432 said "No real RATE_LIMIT_POLICY rule currently uses 'api-key', so we inject one." That was true when written but is now factually wrong — this PR added the MCP rule (tier: 'mcp', key: 'api-key') and PR #211 added the webhooks rule (tier: 'api', key: 'api-key'). The test's logic (inject a synthetic path for bucket isolation) is still correct; only the justification was stale. Updated to: "Inject a synthetic path so this test's bucket is isolated from the live mcp/webhooks rules — both use 'api-key' keying in the production policy and would otherwise share buckets with real traffic." Comment-only change; no test behaviour or assertions touched. Full suite still green (17,718 tests). Co-Authored-By: Claude Opus 4.7 --- tests/unit/lib/security/rate-limit-middleware.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/lib/security/rate-limit-middleware.test.ts b/tests/unit/lib/security/rate-limit-middleware.test.ts index 3511a7d4d..379b0d265 100644 --- a/tests/unit/lib/security/rate-limit-middleware.test.ts +++ b/tests/unit/lib/security/rate-limit-middleware.test.ts @@ -429,8 +429,9 @@ describe('applyRateLimit', () => { }); it("'api-key' key extracts from Authorization: Bearer header", async () => { - // Arrange: inject a synthetic rule using the 'api-key' strategy. - // No real RATE_LIMIT_POLICY rule currently uses 'api-key', so we inject one. + // Arrange: inject a synthetic path so this test's bucket is isolated + // from the live mcp/webhooks rules — both use 'api-key' keying in the + // production policy and would otherwise share buckets with real traffic. const apiTierCap = 100; // api tier cap const keyA = 'my-test-key-alpha'; const keyB = 'my-test-key-beta'; From 6d486216d3fb7bd8bb0fca11b9ef5087a72504ca Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Sat, 23 May 2026 01:02:04 +0100 Subject: [PATCH 6/6] fix(security): address /code-review findings on PR #212 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the 3 findings (each at 75 confidence) surfaced by /code-review: 1. Add a dedicated `exportLimiter` per-flow sub-cap (10/min per admin user) and switch `conversations/export/route.ts` to use it instead of calling `adminLimiter.check(\`export:${ip}\`)` directly. The original pattern was a custom-token sub-cap that piggy-backed on the section limiter — workable but architecturally awkward: both the JSDoc on `adminLimiter` and `.context/security/rate-limiting.md`'s anti-pattern section explicitly forbid direct handler calls to section limiters. The new `exportLimiter` matches the documented per-flow sub-cap shape (mirrors `contactLimiter`, `uploadLimiter`, etc.), is session-user keyed so two admins in the same office don't share a bucket, and the "section limiters never called from handlers" invariant now holds uniformly across the codebase. 2. Renumber the POST handler step comments in `mcp/route.ts` from `// 3.` – `// 7.` to `// 1.` – `// 5.`. Commit 587e20b4 deleted the original step 1 (IP-level rate limit) and converted step 2 to prose, but left the remaining sequence starting at 3 — a non-sequitur for readers following the numbered flow. 3. Add a `vi.mock('@/lib/security/rate-limit', ...)` block to the export route's test file. Pre-fix, the codemod that swept the test had removed the rate-limit mock; combined with the source still calling the real `adminLimiter` directly, this risked LRU bucket bleed across test invocations under parallelism. With the route now using `exportLimiter` (mocked explicitly in this commit), the test exercises a controlled stub. Removed the dead `vi.mock('@/lib/security/ip')` block at the same time since the route no longer reads `getClientIP` — exports are session-user keyed. Source changes: - lib/security/constants.ts: add LIMITS.EXPORT = 10 - lib/security/rate-limit.ts: declare `exportLimiter` adjacent to other per-flow sub-caps, with JSDoc explaining the layered model - app/api/v1/admin/orchestration/conversations/export/route.ts: switch from adminLimiter+IP keying to exportLimiter+session-user keying; drop unused getClientIP import - app/api/v1/mcp/route.ts: renumber 3-7 → 1-5 Tests (+1): - rate-limit.test.ts: config-check test for exportLimiter (limit=10) - export/route.test.ts: replace dead ip mock with exportLimiter mock Docs: - .context/security/rate-limiting.md: add exportLimiter row to the per-flow sub-cap catalogue Validation: - Type-check, lint, format: clean - Full suite: 850/850 files, 17,719 tests (+1), 6 skipped, 10 todo - No section limiter is now called directly from any handler — the documented invariant holds without exception. Co-Authored-By: Claude Opus 4.7 --- .context/security/rate-limiting.md | 1 + .../conversations/export/route.ts | 10 ++++----- app/api/v1/mcp/route.ts | 10 ++++----- lib/security/constants.ts | 7 +++++++ lib/security/rate-limit.ts | 18 ++++++++++++++++ .../conversations/export/route.test.ts | 10 +++++++-- tests/unit/lib/security/rate-limit.test.ts | 21 +++++++++++++++++++ 7 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.context/security/rate-limiting.md b/.context/security/rate-limiting.md index 5c2215780..fdf7232f5 100644 --- a/.context/security/rate-limiting.md +++ b/.context/security/rate-limiting.md @@ -121,6 +121,7 @@ These are NOT tiers. They're tighter caps on specific expensive operations, appl | `inviteLimiter` | 10/15min per IP | Sending invitations | | `uploadLimiter` | 10/15min per IP | File uploads | | `cspReportLimiter` | 20/min per IP | CSP violation reports | +| `exportLimiter` | 10/min per admin user | Bulk-export endpoints (conversations export) | | `agentChatLimiter` | per-agent RPM (dynamic) | Consumer chat with `rateLimitRpm` override | | `apiKeyChatLimiter` | per-key RPM (dynamic) | Webhook triggers with `rateLimitRpm` override | diff --git a/app/api/v1/admin/orchestration/conversations/export/route.ts b/app/api/v1/admin/orchestration/conversations/export/route.ts index a1aec7df0..e0bd23744 100644 --- a/app/api/v1/admin/orchestration/conversations/export/route.ts +++ b/app/api/v1/admin/orchestration/conversations/export/route.ts @@ -18,8 +18,7 @@ import { withAdminAuth } from '@/lib/auth/guards'; import { prisma } from '@/lib/db/client'; import { csvEscape } from '@/lib/api/csv'; import { getRouteLogger } from '@/lib/api/context'; -import { adminLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; -import { getClientIP } from '@/lib/security/ip'; +import { exportLimiter, createRateLimitResponse } from '@/lib/security/rate-limit'; import { conversationExportQuerySchema } from '@/lib/validations/orchestration'; /** Maximum conversations per export to prevent memory issues. */ @@ -29,9 +28,10 @@ const MAX_EXPORT_CONVERSATIONS = 500; const MAX_MESSAGES_PER_CONVERSATION = 500; export const GET = withAdminAuth(async (request, session) => { - // Extra rate limit for exports — 1/min per admin IP - const ip = getClientIP(request); - const rl = adminLimiter.check(`export:${ip}`); + // Per-flow sub-cap on top of the orchestration section tier (which the + // proxy applies upstream at 120/min). Exports are bulk reads — tighter + // dedicated bucket at 10/min per admin user. + const rl = exportLimiter.check(`export:user:${session.user.id}`); if (!rl.success) return createRateLimitResponse(rl); const log = await getRouteLogger(request); diff --git a/app/api/v1/mcp/route.ts b/app/api/v1/mcp/route.ts index 1c1106b8a..1a2c90249 100644 --- a/app/api/v1/mcp/route.ts +++ b/app/api/v1/mcp/route.ts @@ -58,7 +58,7 @@ export async function POST(request: NextRequest): Promise { ); } - // 3. Check MCP server is enabled + // 1. Check MCP server is enabled const serverState = await getMcpServerConfig(); if (!serverState.isEnabled) { return Response.json( @@ -71,7 +71,7 @@ export async function POST(request: NextRequest): Promise { ); } - // 4. Parse request body with size limit + // 2. Parse request body with size limit const contentLength = request.headers.get('content-length'); if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) { return Response.json( @@ -98,7 +98,7 @@ export async function POST(request: NextRequest): Promise { ); } - // 5. Detect batch vs single request + // 3. Detect batch vs single request const isBatch = Array.isArray(rawBody); const rawArray = isBatch ? (rawBody as unknown[]) : null; @@ -151,7 +151,7 @@ export async function POST(request: NextRequest): Promise { const sessionManager = getMcpSessionManager(); const rateLimiter = getMcpRateLimiter(); - // 6. Session management + // 4. Session management const hasInitialize = validRequests.some((r) => r.method === 'initialize'); const sessionId = request.headers.get(MCP_SESSION_HEADER); let session; @@ -231,7 +231,7 @@ export async function POST(request: NextRequest): Promise { ); } - // 7. Dispatch each request + // 5. Dispatch each request const handlerContext = { auth, session, serverState, rateLimiter }; const responses: (JsonRpcResponse | null)[] = []; diff --git a/lib/security/constants.ts b/lib/security/constants.ts index 3e7c8cfd3..74849b043 100644 --- a/lib/security/constants.ts +++ b/lib/security/constants.ts @@ -76,6 +76,13 @@ export const SECURITY_CONSTANTS = { CONSUMER_CHAT: 10, /** Audio transcription: 10 requests per minute per user/session */ AUDIO: 10, + /** + * Conversation export (admin): 10 requests per minute per user. + * Per-flow sub-cap on top of the orchestration section tier. Export + * routes are bulk reads — building a JSON/CSV file from many rows — + * so they get a dedicated bucket that's tighter than the section cap. + */ + EXPORT: 10, /** Image / PDF attachment chat turn: 20 requests per minute per user/session */ IMAGE: 20, }, diff --git a/lib/security/rate-limit.ts b/lib/security/rate-limit.ts index 2b832e012..7a4905377 100644 --- a/lib/security/rate-limit.ts +++ b/lib/security/rate-limit.ts @@ -387,6 +387,24 @@ export const embedChatLimiter = createRateLimiter({ uniqueTokenPerInterval: SECURITY_CONSTANTS.RATE_LIMIT.MAX_UNIQUE_TOKENS, }); +/** + * Rate limiter for admin bulk-export endpoints (e.g. conversation export). + * Limit: 10 requests per minute per user. + * + * Export routes are bulk reads — building a JSON/CSV file from many rows + * is heavier than the typical orchestration list/edit operation. The + * orchestration section tier (120/min) already applies via the middleware; + * this per-flow sub-cap is the additive defence specifically for the + * expensive read pattern. Keyed on the admin user ID via + * `export:user:${session.user.id}` so two admins in the same office don't + * share a bucket. + */ +export const exportLimiter = createRateLimiter({ + interval: SECURITY_CONSTANTS.RATE_LIMIT.DEFAULT_INTERVAL, + maxRequests: SECURITY_CONSTANTS.RATE_LIMIT.LIMITS.EXPORT, + uniqueTokenPerInterval: SECURITY_CONSTANTS.RATE_LIMIT.MAX_UNIQUE_TOKENS, +}); + /** * Inbound trigger limiter — 60 requests per minute per (channel + remote IP). * Slack can burst on app-mention storms; Postmark inbound is steadier. The diff --git a/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts b/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts index 0712d60c1..14b800e18 100644 --- a/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts +++ b/tests/unit/app/api/v1/admin/orchestration/conversations/export/route.test.ts @@ -26,8 +26,14 @@ vi.mock('@/lib/db/client', () => ({ }, })); -vi.mock('@/lib/security/ip', () => ({ - getClientIP: vi.fn(() => '127.0.0.1'), +vi.mock('@/lib/security/rate-limit', () => ({ + // Per-flow sub-cap for export routes (introduced alongside this test pass). + // Default: allow every request. Tests that need to drive the 429 path + // override the mock return value inline. + exportLimiter: { check: vi.fn(() => ({ success: true, limit: 10, remaining: 9, reset: 0 })) }, + createRateLimitResponse: vi.fn(() => + Response.json({ success: false, error: { code: 'RATE_LIMITED' } }, { status: 429 }) + ), })); vi.mock('@/lib/api/context', () => ({ diff --git a/tests/unit/lib/security/rate-limit.test.ts b/tests/unit/lib/security/rate-limit.test.ts index b214a06ef..3b6b8430d 100644 --- a/tests/unit/lib/security/rate-limit.test.ts +++ b/tests/unit/lib/security/rate-limit.test.ts @@ -18,6 +18,7 @@ import { adminLimiter, orchestrationAdminLimiter, mcpLimiter, + exportLimiter, RATE_LIMIT_TIERS, getRateLimitHeaders, createRateLimitResponse, @@ -239,6 +240,26 @@ describe('Rate Limiter', () => { passwordResetLimiter.reset(token); } }); + + it('exportLimiter is configured with the export sub-cap (10/min)', () => { + // Arrange: per-flow sub-cap for bulk-read admin endpoints (e.g. the + // conversation export route). Sits beneath the orchestration tier + // (120/min, applied by the middleware) and provides additional + // protection against runaway export pulls. + const token = `export-test-${Date.now()}`; + + try { + // Act + const result = exportLimiter.check(token); + + // Assert: 10/min is what the EXPORT constant resolves to via envInt + expect(result.limit).toBe(10); + // test-review:accept tobe_true — structural assertion verifying the limiter accepted the first request + expect(result.success).toBe(true); + } finally { + exportLimiter.reset(token); + } + }); }); describe('createDynamicLimiter', () => {