import { PlanFeature, planHasFeature } from '../value-objects/plan-feature.vo'; import { SubscriptionPlan, SubscriptionPlanType } from '../value-objects/subscription-plan.vo'; import { effectivePlan } from './subscription-access'; /** * Politique d'acces aux capacites exposees par l'assistant et par le serveur MCP. * * Une capacite est une action du produit rendue appelable par un agent. Elle * n'est pas decrite dans un prompt : elle est declaree ici avec ce qu'elle * exige, et le controle a lieu dans le processus, sur l'identite authentifiee. * Un modele peut se tromper de mot, il ne peut pas se donner un role. * * Deux conditions, verifiees dans cet ordre : * * 1. **Le role** — qui a le droit d'agir (`ADMIN`, `MANAGER`, `USER`...). * 2. **L'offre** — ce que l'abonnement de l'organisation ouvre, via les memes * `PLAN_FEATURES` que le reste du produit. * * L'offre effective passe par `effectivePlan` : un compte ADMIN dispose de * Platinium, exactement comme dans l'apercu d'abonnement et dans l'assistant. */ /** `read` n'ecrit rien ; `write` modifie l'etat du produit. */ export type CapabilityScope = 'read' | 'write'; export interface CapabilityPolicy { /** Identifiant stable, expose tel quel aux clients MCP. */ name: string; scope: CapabilityScope; /** Roles autorises. Absent : tout compte authentifie. */ roles?: readonly string[]; /** Fonctionnalite d'offre requise. Absent : aucune condition d'abonnement. */ feature?: PlanFeature; } export interface CapabilityActor { id: string; organizationId: string; role?: string; /** Adresse de l'appelant, reportee telle quelle dans le journal d'audit. */ email?: string; /** Offre de l'organisation. Inconnue ou absente : Bronze. */ plan?: string; } /** * Offre effective de l'appelant. * * Une valeur inconnue retombe sur Bronze, l'offre la plus restrictive, plutot * que de faire echouer l'appel ou — pire — de l'autoriser par defaut. */ export function actorPlan(actor: CapabilityActor): SubscriptionPlanType { let declared: SubscriptionPlan | null = null; try { if (actor.plan) declared = SubscriptionPlan.fromString(actor.plan); } catch { declared = null; } return effectivePlan(actor.role, declared).value; } export function canInvoke(actor: CapabilityActor, policy: CapabilityPolicy): boolean { if ( policy.roles && !policy.roles.some(role => role.toLowerCase() === actor.role?.toLowerCase()) ) { return false; } if (policy.feature && !planHasFeature(actorPlan(actor), policy.feature)) { return false; } return true; } /** * Filtre un catalogue pour un appelant. * * Une capacite hors de ses droits n'est pas seulement refusee a l'appel : elle * n'apparait pas dans la liste. Un agent ne peut pas proposer, ni meme * mentionner, une action que la personne n'a pas le droit de declencher. */ export function grantedCapabilities( actor: CapabilityActor, capabilities: readonly T[] ): T[] { return capabilities.filter(capability => canInvoke(actor, capability.policy)); } /** Raison du refus, destinee au message d'erreur rendu a l'agent. */ export function denialReason( actor: CapabilityActor, policy: CapabilityPolicy ): 'role' | 'plan' | null { if (canInvoke(actor, policy)) return null; if (policy.roles && !policy.roles.some(r => r.toLowerCase() === actor.role?.toLowerCase())) { return 'role'; } return 'plan'; }