Skip to content

fix(api): enforce API key policy on ElevenLabs proxy routes (#12574) - #13244

Merged
diegosouzapw merged 1 commit into
release/v3.8.51from
fix/12574-elevenlabs-policy
Sep 12, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.51from
fix/12574-elevenlabs-policy

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Closes #12574

Root cause

The three ElevenLabs-backed /v1/* routes (speech-to-text, text-to-speech/[voiceId],
voices) all funnel through the shared proxyElevenLabsRequest() in
src/app/api/v1/_shared/elevenLabsProxy.ts, which went straight from credential lookup to
the upstream fetch() and never called enforceApiKeyPolicy(). Every other billable
/v1/* route (images, audio, videos, music, ocr, rerank, moderations, embeddings, search,
etc.) calls enforceApiKeyPolicy before dispatch. Because the clientApiPolicy middleware
only validates key validity/ban/expiry (not budget, custom rate limits, schedule windows, or
allowedEndpoints), any valid API key could call these three routes completely unconstrained
by an operator's budget caps or rate limits.

A second, related gap: even with enforceApiKeyPolicy() added, allowedEndpoints scoping
would still no-op on these three routes because ENDPOINT_CATEGORIES
(src/shared/constants/endpointCategories.ts) only mapped /v1/audio to the audio
category — resolveEndpointCategory() returned null for /v1/speech-to-text,
/v1/text-to-speech, and /v1/voices, so validateEndpointAccess() never fired for them.

Fix

  • src/app/api/v1/_shared/elevenLabsProxy.ts: call enforceApiKeyPolicy(request, null) at
    the top of proxyElevenLabsRequest(), before the credential lookup, matching the pattern
    used by 21 sibling /v1/* routes (e.g. src/app/api/v1/images/upscale/route.ts). A
    rejected key never touches ElevenLabs credentials or issues an upstream request.
  • src/shared/constants/endpointCategories.ts: added a new elevenlabs category with
    prefixes /v1/speech-to-text, /v1/text-to-speech, /v1/voices — a dedicated category
    rather than extending audio, so existing keys already scoped to audio (expecting only
    /v1/audio/*) are not silently widened.
  • tests/unit/elevenlabs-policy.test.ts (new): budget-exceeded key gets 429 on all three
    routes without reaching upstream; a key scoped away from the elevenlabs category gets
    403 on all three routes; an unrestricted key still proxies through successfully.
  • tests/unit/endpoint-categories.test.ts: added 3 cases asserting
    resolveEndpointCategory() now maps the three ElevenLabs paths to elevenlabs instead of
    null.

Regression test — RED → GREEN

node --import tsx/esm --test tests/unit/elevenlabs-policy.test.ts

RED (on unfixed code, verified by temporarily reverting the two production files):

✖ an over-budget key is rejected with 429 on all three ElevenLabs routes without reaching upstream
  AssertionError: 200 !== 429
✖ a key scoped away from the elevenlabs category is rejected with 403 on all three routes
  AssertionError: 200 !== 403

GREEN (with the fix):

✔ an over-budget key is rejected with 429 on all three ElevenLabs routes without reaching upstream
✔ a key scoped away from the elevenlabs category is rejected with 403 on all three routes
✔ an unrestricted key still proxies through to the upstream successfully
ℹ tests 3
ℹ pass 3
ℹ fail 0

Gates run (all green)

  • node scripts/check/check-file-size.mjs — OK, no frozen file grown
  • node scripts/check/check-complexity.mjs — OK (2799 violations vs baseline 3218)
  • node scripts/check/check-cognitive-complexity.mjs — OK (1265 violations vs baseline 1437)
  • npm run typecheck:core — exit 0
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — exit 0
  • tests/unit/elevenlabs-policy.test.ts (new) — 3/3 pass
  • tests/unit/endpoint-categories.test.ts (existing + 3 new cases) — 29/29 pass
  • tests/unit/endpoint-restrictions-policy.test.ts (existing) — 11/11 pass
  • tests/unit/api-key-policy.test.ts (existing) — 41/41 pass
  • tests/unit/elevenlabs-native-routes.test.ts (existing) — 6/6 pass, no regression
  • node scripts/check/check-changelog-integrity.mjs — OK

⚠️ base-red inherited: #12732 — unit #12058, integration codex-cache, package-artifact, tarball-smoke, agent-skills-sync

@diegosouzapw
diegosouzapw merged commit bc28bb0 into release/v3.8.51 Sep 12, 2026
15 of 21 checks passed
Githab-capibara added a commit to Githab-capibara/OmniRoute that referenced this pull request Sep 17, 2026
…inference#12574) (cheaper-inference#13244)

Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on cheaper-inference#13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for cheaper-inference#13278 and cheaper-inference#13276, annotated and landed on cheaper-inference#13243

⚠️ base-red inherited: cheaper-inference#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…inference#12574) (cheaper-inference#13244)

Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on cheaper-inference#13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for cheaper-inference#13278 and cheaper-inference#13276, annotated and landed on cheaper-inference#13243

⚠️ base-red inherited: cheaper-inference#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
@diegosouzapw
diegosouzapw deleted the fix/12574-elevenlabs-policy branch October 18, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): ElevenLabs proxy routes (speech-to-text, text-to-speech, voices) skip enforceApiKeyPolicy

1 participant