plan-features.customDomains is now { web, mail } per plan instead of a
single number. Free 5+5, Pro 5+5, Legend 10+10 — the user explicitly
chose separate pools so users don't have to trade a website slot for a
mail-pattern slot or vice versa.
- countActiveCustomDomainsSplit(userId) groupBy type → { web, mail }
(mail aggregates mail_domain + mail_display_name). Old single-count
function stays as a deprecated alias for any caller still on it.
- POST /api/custom-domains: body-compat accepts both { pattern, kind }
(current frontend) and { domain, type } (legacy / direct). kind='mail'
is split into mail_domain vs mail_display_name server-side based on
whether the pattern looks like a domain. Slot check is per-bucket;
errors are WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED so the UI can show
the right limit-reached message per tab.
- GET /api/custom-domains: response shape extended to
{ items, counts: { web, mail }, limits: { web, mail } } so the
frontend can drive the per-tab counter without client-side estimation.
- POST /api/custom-domains/:id/submit: hard-blocks mail_display_name
with 400 DISPLAY_NAME_NOT_SUBMITTABLE. Display-name submission to the
global blocklist is deferred to v1.1 — would require a schema split
on BlocklistDomain that's risky pre-TestFlight. mail_domain still
flows through the community-vote pipeline like web entries.
- auth/me.get.ts, plan/change-preview.get.ts, coach/message.post.ts
updated for the new shape (Lyra prompts untouched, only template
variables split web vs mail counts).
24 vitest cases in backend/tests/custom-domains/plan-limits.test.ts
cover the new shape, body compat, bucket logic, and the submit guard;
216/216 total backend tests pass.
208 lines
7.4 KiB
TypeScript
208 lines
7.4 KiB
TypeScript
export type Plan = "free" | "pro" | "legend";
|
|
|
|
export type VoiceProvider = "elevenlabs" | "openai" | "google" | "cartesia" | "azure";
|
|
|
|
export interface VoiceConfig {
|
|
/** TTS-Provider für diesen Plan */
|
|
provider: VoiceProvider;
|
|
/** Provider-spezifische Model-ID (optional) */
|
|
model?: string;
|
|
/** Provider-spezifische Voice-ID (optional — fällt auf provider-default zurück) */
|
|
voiceId?: string;
|
|
/** Tages-Quota in Sekunden. 0 = unlimited */
|
|
dailyQuotaSeconds: number;
|
|
}
|
|
|
|
export interface CustomDomainLimits {
|
|
/** Max. Web-Domain-Slots (Infinity = unbegrenzt) */
|
|
web: number;
|
|
/** Max. Mail-Slots — kombiniert für mail_domain + mail_display_name */
|
|
mail: number;
|
|
}
|
|
|
|
export interface PlanLimits {
|
|
// ─── Custom Domains ──────────────────────────────────────────────────────
|
|
/** Max. eigene Domains aufgeteilt nach Typ-Bucket */
|
|
customDomains: CustomDomainLimits;
|
|
/** Freigeschaltete Domain-Slots füllen sich wieder auf (Community-Promotion) */
|
|
domainRefill: boolean;
|
|
|
|
// ─── Mail-Accounts ───────────────────────────────────────────────────────
|
|
/** Max. aktive Mail-Agenten (Infinity = unbegrenzt) */
|
|
mailAgents: number;
|
|
/** Erlaubte Scan-Intervalle in Stunden */
|
|
mailIntervalOptions: number[];
|
|
|
|
// ─── Globale Blocklist ───────────────────────────────────────────────────
|
|
/**
|
|
* 'curated' = kleiner Stub der bekanntesten Casino-Domains (Free).
|
|
* 'full' = vollständige HaGeZi/ReBreak-Liste (~208k).
|
|
*
|
|
* Der Stub selbst ist in server/utils/curated-blocklist.ts definiert.
|
|
* Die echte ~1-2k HaGeZi-Subset-Liste ist ein separates Daten-Ticket.
|
|
*/
|
|
globalBlocklist: "curated" | "full";
|
|
|
|
// ─── Community ───────────────────────────────────────────────────────────
|
|
/** Darf in der Community posten */
|
|
canPost: boolean;
|
|
/** Darf Gruppen gründen */
|
|
canCreateGroup: boolean;
|
|
/** Darf Domains direkt zur ReBreak Blocklist hinzufügen */
|
|
canAddToBlocklist: boolean;
|
|
|
|
// ─── Geräte (zwei getrennte Konzepte!) ───────────────────────────────────
|
|
/**
|
|
* Max. parallel eingeloggte App-Geräte pro Account (Anti-Account-Sharing).
|
|
* Bezieht sich auf UserDevice (iOS/Android-App-Instanzen).
|
|
*/
|
|
maxAppDevices: number;
|
|
/**
|
|
* Max. zusätzliche Geräte (Mac/Windows) die per DNS-Profil geschützt werden.
|
|
* Bezieht sich auf ProtectedDevice (Legend-only Feature).
|
|
* 0 = Feature nicht verfügbar.
|
|
*/
|
|
maxProtectedDevices: number;
|
|
|
|
// ─── AI-Coach ────────────────────────────────────────────────────────────
|
|
/** Primäres OpenRouter/Groq-Modell für KI-Coach */
|
|
aiModel: string;
|
|
/** Fallback-Modelle (werden der Reihe nach versucht wenn primary fehlschlägt) */
|
|
aiModelFallbacks: Array<{ provider: "groq" | "openrouter"; model: string }>;
|
|
/** AI-Provider: groq (Free/Pro) oder openrouter (Legend/Claude) */
|
|
aiProvider: "groq" | "openrouter";
|
|
|
|
// ─── TTS ─────────────────────────────────────────────────────────────────
|
|
/**
|
|
* Voice-Config: welcher TTS-Provider + Quota.
|
|
*
|
|
* Provider-Mapping (Cost-Reference 2026-05):
|
|
* Free → Google TTS Neural2 (~$4/1M chars, 60s/day cap)
|
|
* Pro → Cartesia Sonic-2 (~$4/1M chars, 300s/day cap, ~75ms first-byte)
|
|
* Legend → ElevenLabs Turbo v2.5 (~$30/1M chars, unlimited)
|
|
*/
|
|
voice: VoiceConfig;
|
|
}
|
|
|
|
export const PLAN_LIMITS: Record<Plan, PlanLimits> = {
|
|
free: {
|
|
customDomains: { web: 5, mail: 5 },
|
|
domainRefill: false,
|
|
mailAgents: 1,
|
|
mailIntervalOptions: [4],
|
|
globalBlocklist: "curated",
|
|
canPost: true,
|
|
canCreateGroup: false,
|
|
canAddToBlocklist: false,
|
|
maxAppDevices: 1,
|
|
maxProtectedDevices: 0,
|
|
aiModel: "llama-3.1-8b-instant",
|
|
aiModelFallbacks: [
|
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
|
{ provider: "groq", model: "gemma2-9b-it" },
|
|
{ provider: "openrouter", model: "meta-llama/llama-3.1-8b-instruct" },
|
|
],
|
|
aiProvider: "groq",
|
|
voice: {
|
|
provider: "google",
|
|
model: "de-DE-Neural2-F", // Google Cloud TTS Neural2 — natural, ~$4/1M chars
|
|
dailyQuotaSeconds: 60, // 1 Minute/Tag
|
|
},
|
|
},
|
|
pro: {
|
|
customDomains: { web: 5, mail: 5 },
|
|
domainRefill: true,
|
|
mailAgents: 3,
|
|
mailIntervalOptions: [1, 4, 8],
|
|
globalBlocklist: "full",
|
|
canPost: true,
|
|
canCreateGroup: false,
|
|
canAddToBlocklist: false,
|
|
maxAppDevices: 1,
|
|
maxProtectedDevices: 0,
|
|
aiModel: "llama-3.3-70b-versatile",
|
|
aiModelFallbacks: [
|
|
{ provider: "groq", model: "llama-3.1-8b-instant" },
|
|
{ provider: "openrouter", model: "meta-llama/llama-3.3-70b-instruct" },
|
|
],
|
|
aiProvider: "groq",
|
|
voice: {
|
|
provider: "cartesia",
|
|
model: "sonic-2", // Cartesia Sonic-2 — ~75ms TTFT, native German, ~$4/1M chars
|
|
dailyQuotaSeconds: 300, // 5 Minuten/Tag
|
|
},
|
|
},
|
|
legend: {
|
|
customDomains: { web: 10, mail: 10 },
|
|
domainRefill: true,
|
|
mailAgents: Infinity,
|
|
mailIntervalOptions: [1, 4, 8],
|
|
globalBlocklist: "full",
|
|
canPost: true,
|
|
canCreateGroup: true,
|
|
canAddToBlocklist: true,
|
|
maxAppDevices: 3,
|
|
maxProtectedDevices: 2, // "+2 weitere Geräte" (§0.5)
|
|
aiModel: "anthropic/claude-3.5-haiku",
|
|
aiModelFallbacks: [
|
|
{ provider: "openrouter", model: "anthropic/claude-3-haiku" },
|
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
|
],
|
|
aiProvider: "openrouter",
|
|
voice: {
|
|
provider: "elevenlabs",
|
|
model: "eleven_turbo_v2_5", // ElevenLabs Turbo v2.5 — premium, ~$30/1M chars
|
|
dailyQuotaSeconds: 0, // 0 = unlimited
|
|
},
|
|
},
|
|
};
|
|
|
|
export function getPlanLimits(plan: string): PlanLimits {
|
|
// Legacy-Pläne auf neue Namen mappen
|
|
if (plan === "premium") return PLAN_LIMITS.legend;
|
|
if (plan === "standard") return PLAN_LIMITS.pro;
|
|
return PLAN_LIMITS[(plan as Plan) ?? "free"] ?? PLAN_LIMITS.free;
|
|
}
|
|
|
|
/**
|
|
* Kuratierter Stub der bekanntesten Glücksspiel-Domains für Free-User.
|
|
* Diese Liste ist der Mechanismus — der echte ~1-2k HaGeZi-Subset
|
|
* ist ein separates Daten-Ticket (TODO: Daten-Ticket anlegen).
|
|
*
|
|
* Wird in DNS-Blocklist-Endpoints und scan-internal verwendet wenn
|
|
* limits.globalBlocklist === 'curated'.
|
|
*/
|
|
export const CURATED_BLOCKLIST_STUB: string[] = [
|
|
// DE / Offshore-Klassiker (Top-Tier-Traffic)
|
|
"betway.com",
|
|
"bet365.com",
|
|
"888casino.com",
|
|
"pokerstars.com",
|
|
"williamhill.com",
|
|
"bwin.com",
|
|
"unibet.com",
|
|
"partypoker.com",
|
|
"casinoclub.com",
|
|
"interwetten.com",
|
|
"tipico.de",
|
|
"betsson.com",
|
|
"casumo.com",
|
|
"leovegas.com",
|
|
"mr-green.com",
|
|
"jackpot.de",
|
|
"sunmaker.com",
|
|
"stargames.com",
|
|
"mybet.com",
|
|
"winner.com",
|
|
"ladbrokes.com",
|
|
"coral.co.uk",
|
|
"paddypower.com",
|
|
"betfair.com",
|
|
"mrvegas.com",
|
|
"slotsmillion.com",
|
|
"casinoeuropa.com",
|
|
"netbet.com",
|
|
"platincasino.com",
|
|
"euslot.com",
|
|
];
|