Skip to content

fix(tts): rate-limit bypass, silent breaker DoS, dead tracing SDK, and no shared state under scaling - #1250

Merged
hman38705 merged 5 commits into
mainfrom
fix/tts-security-reliability-batch
Jul 28, 2026
Merged

fix(tts): rate-limit bypass, silent breaker DoS, dead tracing SDK, and no shared state under scaling#1250
hman38705 merged 5 commits into
mainfrom
fix/tts-security-reliability-batch

Conversation

@hman38705

Copy link
Copy Markdown
Contributor

Conflict-resolved re-submission of #1247 (fork PR, no push access to update it directly). Merges main in and reconciles the tenant-scoped job store (from #1246) with the async/shared-store job paths added here, so cross-tenant isolation is preserved on the scaled-out code paths too. Closes #1247 in favor of this.

Fidelis900 and others added 5 commits July 28, 2026 06:25
… header

The Express rate-limit keyGenerator used the raw Authorization header as
the bucket key before auth middleware validated anything, letting a caller
rotate a fake Bearer token per request to get a fresh bucket every time and
bypass the per-IP throttle on /tts/generate and /tts/enqueue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…unting

opossumOptions never set errorFilter, so non-retriable 4xx TTSProviderErrors
(bad voice, invalid credential) counted toward tripping the breaker exactly
like 5xx/network failures. A handful of bad requests from one caller could
open the breaker for every user of that provider. errorFilter now excludes
4xx TTSProviderErrors from failure stats while still rejecting the call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
initTracing() set up the NodeSDK, OTLP exporter, and HTTP instrumentation
but was never invoked anywhere, so every tracer.startActiveSpan(...) call
in TTSService/HealthCheck ran against the no-op global tracer provider and
spans were silently discarded. Now invoked once at server module load,
before any request handling. app.listen() is guarded behind a require.main
check so importing server.ts (e.g. from tests) no longer binds a real port.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hared store

jobStore, RateLimiter, and AudioCache were process-local Maps with no shared
backing, so under horizontal scaling GET /tts/job/:id 404s when polling hits
a different replica than the one that processed the job, rate limits are
only enforced per-replica (multiplying the effective global limit by
replica count), and cache hit rate collapses across replicas.

Adds a SharedStore abstraction (SharedJobStore/SharedRateLimitStore/
SharedCacheStore) with a Redis-backed implementation, wired in via
config.sharedStore / REDIS_URL. New enqueueAsync/getJobAsync/listJobsAsync
methods use it when configured, falling back to the existing in-memory,
single-instance behavior otherwise — enqueue/getJob/listJobs are unchanged
for backward compatibility. server.ts routes now use the async methods.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…scoped job store with async/shared-store paths
@hman38705
hman38705 merged commit 0b1b97c into main Jul 28, 2026
0 of 10 checks passed
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.

2 participants