Files:
apps/web/src/lib/plan-sla.ts lines 4–13 (slaHoursForPlan), 18–27 (domainCapForPlan)
packages/jobs/src/takedown.ts lines 74–85 (slaHoursForPlan duplicate)
Vulnerability:
Both slaHoursForPlan and domainCapForPlan accept plan: string and fall through to a default branch that silently assigns entry-tier values (72 h SLA, 25-domain cap) for any unexpected input:
export function slaHoursForPlan(plan: string): number {
switch (plan) {
case "enterprise": return 2;
case "mid": return 24;
default: return 72; // ← silently hit by null, "", or a new enum value
}
}
If org.plan is ever null (raw SQL update, future migration bug, or an admin bypassing Drizzle) or a new enum value is added without updating these switches, enterprise orgs silently receive:
- 72-hour DMCA SLA instead of 2 hours (breach of the enterprise contract; the 1-hour escalation gate in goals.md never fires)
- 25-domain cap instead of 5000 (import rejected for legitimate enterprise usage)
The organizations.plan column is notNull() today, but the defensive gap is the silent default — no warning is raised and the caller can't distinguish "entry plan" from "unexpected plan value".
Fix:
- Narrow the parameter type to
Plan (the branded union type already in org-context.tsx) so TypeScript catches missing cases at compile time.
- Add a runtime guard that logs a warning on unrecognized values:
export function slaHoursForPlan(plan: Plan | string): number {
switch (plan) {
case "enterprise": return 2;
case "mid": return 24;
case "entry": return 72;
default:
console.error(`[slaHoursForPlan] unknown plan "${plan}" — defaulting to 72h`);
return 72;
}
}
- Sync the duplicate
slaHoursForPlan in packages/jobs/src/takedown.ts (lines 74–85) with the same guard, or better: extract both into a shared @repo/plan-sla package.
Files:
apps/web/src/lib/plan-sla.tslines 4–13 (slaHoursForPlan), 18–27 (domainCapForPlan)packages/jobs/src/takedown.tslines 74–85 (slaHoursForPlanduplicate)Vulnerability:
Both
slaHoursForPlananddomainCapForPlanacceptplan: stringand fall through to adefaultbranch that silently assigns entry-tier values (72 h SLA, 25-domain cap) for any unexpected input:If
org.planis evernull(raw SQL update, future migration bug, or an admin bypassing Drizzle) or a new enum value is added without updating these switches, enterprise orgs silently receive:The
organizations.plancolumn isnotNull()today, but the defensive gap is the silent default — no warning is raised and the caller can't distinguish "entry plan" from "unexpected plan value".Fix:
Plan(the branded union type already inorg-context.tsx) so TypeScript catches missing cases at compile time.slaHoursForPlaninpackages/jobs/src/takedown.ts(lines 74–85) with the same guard, or better: extract both into a shared@repo/plan-slapackage.