Security & robustness hardening pass

Cross-cutting input-validation, isolation, and DoS-resistance fixes across
the app, API, billing, queue, and infra layers.

- Runtime validation (zod) for client-supplied admin actions (role/plan/
  limits), series generation index, and all pg-boss queue payloads
- Auth: require email verification before sign-in; reject weak/placeholder/
  short BETTER_AUTH_SECRET in production
- Billing: sanitize Stripe/PayPal errors (log server-side, generic to client);
  race-safe subscription upsert; only count "processed" webhook events as
  handled; verify org membership in getEffectivePlan to block plan escalation
- Series generation: reserve usage up front and refund on failure; bill the
  owning org, not the caller's active org
- Injection defenses: HTML-escape user fields in emails, strip CR/LF from
  subject/recipient, validate ElevenLabs voiceId before URL interpolation
- Media routes: stream off disk instead of buffering whole files; rate-limit
  anonymous public audio/cover endpoints by client IP
This commit is contained in:
Leon Serfaty
2026-06-20 20:59:03 -04:00
parent cd1d6a1a28
commit 51c541ad22
21 changed files with 489 additions and 152 deletions
+12 -1
View File
@@ -1,4 +1,4 @@
import { promises as fs } from "node:fs";
import { promises as fs, createReadStream as fsCreateReadStream } from "node:fs";
import path from "node:path";
import type { StorageProvider } from "./types";
@@ -29,6 +29,17 @@ export class LocalStorageProvider implements StorageProvider {
return fs.readFile(resolveSafe(key));
}
/**
* Stream a file (optionally an inclusive byte range) without buffering it all
* into memory. Goes through resolveSafe so path-traversal protection holds.
*/
createReadStream(key: string, range?: { start: number; end: number }): NodeJS.ReadableStream {
const full = resolveSafe(key);
return range
? fsCreateReadStream(full, { start: range.start, end: range.end })
: fsCreateReadStream(full);
}
async exists(key: string): Promise<boolean> {
try {
await fs.access(resolveSafe(key));
+10
View File
@@ -9,6 +9,16 @@ export interface StorageProvider {
put(key: string, data: Buffer | Uint8Array, contentType?: string): Promise<void>;
/** Read the full object as a Buffer. */
get(key: string): Promise<Buffer>;
/**
* Stream the object (optionally a byte range, inclusive) instead of buffering
* it all into memory — used by the audio/asset routes to avoid memory
* amplification under concurrent load. Optional: providers may omit it, in
* which case callers fall back to get().
*/
createReadStream?(
key: string,
range?: { start: number; end: number }
): NodeJS.ReadableStream;
/** Whether an object exists at `key`. */
exists(key: string): Promise<boolean>;
/** Remove the object (no-op if missing). */