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
+31 -17
View File
@@ -1,5 +1,7 @@
/** Queue/job names and their typed payloads. Shared by the web (producer) and worker (consumer). */
import { z } from "zod";
export const QUEUES = {
generateEpisode: "episode.generate",
generateSeries: "series.generate",
@@ -10,29 +12,41 @@ export const QUEUES = {
export type QueueName = (typeof QUEUES)[keyof typeof QUEUES];
export type GenerationType = "full" | "script" | "audio" | "art" | "section" | "repurpose";
export const generationTypeSchema = z.enum([
"full",
"script",
"audio",
"art",
"section",
"repurpose",
]);
export type GenerationType = z.infer<typeof generationTypeSchema>;
export interface GenerateEpisodePayload {
episodeId: string;
export const generateEpisodePayloadSchema = z.object({
episodeId: z.string().min(1),
/** "full" runs the whole pipeline; the others re-run a single stage. */
type?: GenerationType;
type: generationTypeSchema.optional(),
/** For type="section", the script section to regenerate. */
sectionId?: string;
}
sectionId: z.string().optional(),
});
export type GenerateEpisodePayload = z.infer<typeof generateEpisodePayloadSchema>;
export interface RepurposePayload {
episodeId: string;
format: "blog" | "social_thread" | "newsletter";
}
export const repurposePayloadSchema = z.object({
episodeId: z.string().min(1),
format: z.enum(["blog", "social_thread", "newsletter"]),
});
export type RepurposePayload = z.infer<typeof repurposePayloadSchema>;
export interface GenerateSeriesPayload {
seriesId: string;
}
export const generateSeriesPayloadSchema = z.object({
seriesId: z.string().min(1),
});
export type GenerateSeriesPayload = z.infer<typeof generateSeriesPayloadSchema>;
export interface EchoPayload {
message: string;
episodeId?: string;
}
export const echoPayloadSchema = z.object({
message: z.string(),
episodeId: z.string().optional(),
});
export type EchoPayload = z.infer<typeof echoPayloadSchema>;
/** All queues that must exist before send/work. */
export const ALL_QUEUES: QueueName[] = Object.values(QUEUES);