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
+8 -2
View File
@@ -70,7 +70,11 @@ export async function createPaypalSubscription(args: {
},
}),
});
if (!res.ok) throw new Error(`PayPal create subscription ${res.status}: ${await res.text()}`);
if (!res.ok) {
// Log the full upstream detail server-side, but never surface it to clients.
console.error(`[paypal] create subscription ${res.status}: ${await res.text()}`);
throw new Error("PayPal request failed");
}
const data = (await res.json()) as { id: string; links: { rel: string; href: string }[] };
const approveUrl = data.links.find((l) => l.rel === "approve")?.href;
if (!approveUrl) throw new Error("PayPal did not return an approval URL");
@@ -94,7 +98,9 @@ export async function cancelPaypalSubscription(id: string, reason = "Customer re
body: JSON.stringify({ reason }),
});
if (!res.ok && res.status !== 204) {
throw new Error(`PayPal cancel subscription ${res.status}: ${await res.text()}`);
// Log the full upstream detail server-side, but never surface it to clients.
console.error(`[paypal] cancel subscription ${res.status}: ${await res.text()}`);
throw new Error("PayPal request failed");
}
}
+29 -15
View File
@@ -22,16 +22,6 @@ export interface UpsertSubscriptionInput {
* provider subscription id, so duplicate/replayed webhooks converge on one row.
*/
export async function upsertSubscription(input: UpsertSubscriptionInput) {
const existing = input.stripeSubscriptionId
? await prisma.subscription.findFirst({
where: { stripeSubscriptionId: input.stripeSubscriptionId },
})
: input.paypalSubscriptionId
? await prisma.subscription.findFirst({
where: { paypalSubscriptionId: input.paypalSubscriptionId },
})
: null;
const data = {
provider: input.provider,
plan: input.plan,
@@ -47,9 +37,25 @@ export async function upsertSubscription(input: UpsertSubscriptionInput) {
cancelAtPeriodEnd: input.cancelAtPeriodEnd ?? undefined,
};
if (existing) {
return prisma.subscription.update({ where: { id: existing.id }, data });
// Atomic upsert keyed on whichever provider subscription id is present on the
// incoming record. The DB-level @@unique on these columns lets concurrent
// webhook retries converge on a single row instead of racing into duplicates.
if (input.stripeSubscriptionId) {
return prisma.subscription.upsert({
where: { stripeSubscriptionId: input.stripeSubscriptionId },
create: { referenceId: input.referenceId, ...data },
update: data,
});
}
if (input.paypalSubscriptionId) {
return prisma.subscription.upsert({
where: { paypalSubscriptionId: input.paypalSubscriptionId },
create: { referenceId: input.referenceId, ...data },
update: data,
});
}
// Safe fallback: neither provider id is present (no unique key to upsert on),
// so create a fresh row.
return prisma.subscription.create({ data: { referenceId: input.referenceId, ...data } });
}
@@ -85,9 +91,17 @@ export async function getEffectivePlan(
activeOrgId?: string | null
): Promise<{ plan: Plan; key: PlanKey; subjectId: string; subjectType: "user" | "organization" }> {
if (activeOrgId) {
const key = await getSubjectPlanKey(activeOrgId);
if (key !== "free") {
return { plan: getPlan(key), key, subjectId: activeOrgId, subjectType: "organization" };
// Only grant the org's plan if the user is an actual member of that org.
// A stale/forged activeOrganizationId must not elevate a non-member.
const membership = await prisma.member.findUnique({
where: { organizationId_userId: { organizationId: activeOrgId, userId } },
select: { id: true },
});
if (membership) {
const key = await getSubjectPlanKey(activeOrgId);
if (key !== "free") {
return { plan: getPlan(key), key, subjectId: activeOrgId, subjectType: "organization" };
}
}
}
const key = await getSubjectPlanKey(userId);
+3 -1
View File
@@ -3,7 +3,9 @@ import { prisma } from "@/lib/db";
/** True if we've already handled this provider event (idempotency). */
export async function alreadyProcessed(eventId: string): Promise<boolean> {
const existing = await prisma.webhookEvent.findUnique({ where: { eventId } });
return !!existing;
// Only a successfully "processed" event is considered handled. Rows logged as
// "failed" (or "skipped") must be reprocessable when the provider retries.
return existing?.status === "processed";
}
/** Record a webhook delivery for the admin log (best-effort; unique on eventId). */