// packages/core/src/entitlement.ts // Single entitlement contract shared by desktop, web, and mobile. // // The product currently derives access from three sources: Supabase // subscriptions (web/mobile/cloud), the desktop Ed25519 offline license, and // the .NET back-office role. Each source uses a different shape. This module // defines one canonical snapshot and a resolver that maps all three into it so // consumers stop re-implementing tier comparisons. import type { LicenseTier, Feature } from './types' import { Feature as FeatureEnum } from './types' /** Canonical paid tier. `team`/`enterprise` licenses collapse to `pro_plus`. */ export type EntitlementTier = 'free' | 'pro' | 'pro_plus' /** Administrative role shared by the .NET back office and Supabase profiles. */ export type AdminRole = 'user' | 'manager' | 'admin' | 'superadmin' export type EntitlementSource = 'supabase' | 'desktop_license' | 'dotnet' | 'none' export interface EntitlementSnapshot { tier: EntitlementTier /** Original 5-value license tier when the source is a desktop license. */ licenseTier: LicenseTier | null adminRole: AdminRole adminCapabilities: { canManage: boolean canAdmin: boolean canSuperAdmin: boolean } adFree: boolean features: readonly Feature[] source: EntitlementSource expiresAt: number | null refreshedAt: number } export interface ResolveEntitlementInput { subscriptionTier?: unknown licenseTier?: unknown dotnetRole?: unknown supabaseRole?: unknown expiresAt?: number | null refreshedAt?: number } const TIER_RANK: Record = { free: 0, pro: 1, pro_plus: 2, } const ADMIN_RANK: Record = { user: 0, manager: 1, admin: 2, superadmin: 3, } const LOCAL_FEATURES: readonly Feature[] = [ FeatureEnum.DICTATION, FeatureEnum.LLM_PROCESS, FeatureEnum.HISTORY_UNLIMITED, FeatureEnum.HISTORY_EXPORT, FeatureEnum.CUSTOM_INSTRUCTION_CREATE, FeatureEnum.LIVE_CAPTION, FeatureEnum.SCREEN_CONTEXT, FeatureEnum.VOICE_MEMO, FeatureEnum.VOICE_COMMAND, FeatureEnum.LLM_CHAIN, FeatureEnum.FILE_TRANSCRIPTION, FeatureEnum.VOICE_CONVERSATION, FeatureEnum.DICTATION_TEMPLATE, FeatureEnum.MEETING_SUMMARY, FeatureEnum.LOCAL_RAG, FeatureEnum.OS_AUTOMATION, FeatureEnum.PREMIUM_LLM, FeatureEnum.CLOUD_SYNC, ] const PAID_FEATURES: readonly Feature[] = [...LOCAL_FEATURES, FeatureEnum.TEAM_WORKSPACE] export function entitlementFeatures(tier: EntitlementTier): readonly Feature[] { return tier === 'free' ? LOCAL_FEATURES : PAID_FEATURES } export function normalizeEntitlementTier(value: unknown): EntitlementTier { if (typeof value !== 'string') return 'free' const normalized = value.trim().toLowerCase() if (normalized === 'pro') return 'pro' if (normalized === 'pro_plus' || normalized === 'proplus') return 'pro_plus' // Desktop licenses still carry team/enterprise; collapse them to pro_plus. if (normalized === 'team' || normalized === 'enterprise') return 'pro_plus' return 'free' } export function normalizeAdminRole(value: unknown): AdminRole { if (typeof value !== 'string') return 'user' const normalized = value.replace(/[_-]/g, '').trim().toLowerCase() if (normalized === 'superadmin') return 'superadmin' if (normalized === 'admin') return 'admin' if (normalized === 'manager') return 'manager' return 'user' } export function adminRoleAtLeast(role: AdminRole, minimum: AdminRole): boolean { return ADMIN_RANK[role] >= ADMIN_RANK[minimum] } export function maxEntitlementTier(a: EntitlementTier, b: EntitlementTier): EntitlementTier { return TIER_RANK[a] >= TIER_RANK[b] ? a : b } function hasValidPeriod(expiresAt: number | null | undefined, now: number): boolean { if (expiresAt === null || expiresAt === undefined) return true return expiresAt > now } /** * Resolve a single entitlement snapshot from any combination of sources. * * Precedence: * - Tier: the highest paid tier across sources wins (a paid subscription is not * downgraded by a stale free license, and vice versa). * - Admin role: the Supabase profile role wins over the .NET role. * - Paid access requires a valid (non-expired) period. */ export function resolveEntitlement(input: ResolveEntitlementInput = {}): EntitlementSnapshot { const now = input.refreshedAt ?? Date.now() const subscriptionTier = normalizeEntitlementTier(input.subscriptionTier) const licenseTier = typeof input.licenseTier === 'string' ? input.licenseTier : null const normalizedLicenseTier = normalizeEntitlementTier(input.licenseTier) const sources: EntitlementSource[] = [] if (input.subscriptionTier !== undefined && input.subscriptionTier !== null) sources.push('supabase') if (licenseTier !== null) sources.push('desktop_license') if (input.dotnetRole !== undefined && input.dotnetRole !== null) sources.push('dotnet') const periodValid = hasValidPeriod(input.expiresAt ?? null, now) // An expired paid period collapses to free regardless of the source label. const tier: EntitlementTier = periodValid ? maxEntitlementTier(subscriptionTier, normalizedLicenseTier) : 'free' const adminRole = input.supabaseRole !== undefined && input.supabaseRole !== null ? normalizeAdminRole(input.supabaseRole) : normalizeAdminRole(input.dotnetRole) return { tier, licenseTier: (licenseTier as LicenseTier | null) ?? null, adminRole, adminCapabilities: { canManage: adminRoleAtLeast(adminRole, 'manager'), canAdmin: adminRoleAtLeast(adminRole, 'admin'), canSuperAdmin: adminRole === 'superadmin', }, adFree: tier !== 'free' && periodValid, features: entitlementFeatures(tier), source: sources.length > 0 ? sources[0] : 'none', expiresAt: input.expiresAt ?? null, refreshedAt: now, } }