xpeditis2.0/apps/backend/src/domain/services/capability-access.ts
2026-09-07 21:40:53 +02:00

102 lines
3.4 KiB
TypeScript

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<T extends { policy: CapabilityPolicy }>(
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';
}