feat(domain): politique de conservation et d effacement
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
This commit is contained in:
parent
d89104f49e
commit
5a2fb7db8c
@ -46,6 +46,15 @@ export enum AuditAction {
|
||||
// Agent actions — toute capacite invoquee par un agent, via MCP ou via
|
||||
// l'assistant integre. Le nom de la capacite est dans `resourceName`.
|
||||
AGENT_CAPABILITY_INVOKED = 'agent_capability_invoked',
|
||||
|
||||
// Droits des personnes (RGPD). L'article 5.2 impose de pouvoir demontrer
|
||||
// qu'une demande a ete traitee : sans trace, honorer un droit et l'ignorer
|
||||
// se ressemblent. La trace d'un effacement porte l'identifiant technique et
|
||||
// l'adresse anonymisee, jamais l'identite effacee.
|
||||
GDPR_DATA_EXPORTED = 'gdpr_data_exported',
|
||||
GDPR_ERASURE_EXECUTED = 'gdpr_erasure_executed',
|
||||
GDPR_CONSENT_RECORDED = 'gdpr_consent_recorded',
|
||||
GDPR_RETENTION_PURGE = 'gdpr_retention_purge',
|
||||
}
|
||||
|
||||
export enum AuditStatus {
|
||||
|
||||
157
apps/backend/src/domain/services/data-retention.ts
Normal file
157
apps/backend/src/domain/services/data-retention.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Politique de conservation et d'effacement.
|
||||
*
|
||||
* ⚠️ Ce fichier traduit en code des choix **juridiques**, pas techniques. Les
|
||||
* durées ci-dessous doivent être validées par un conseil avant mise en
|
||||
* production : elles sont regroupées ici précisément pour être relues d'un
|
||||
* seul tenant, plutôt que dispersées dans les services.
|
||||
*
|
||||
* L'effacement (RGPD art. 17) ne peut pas être un `DELETE` généralisé : une
|
||||
* partie des données répond à une obligation légale de conservation qui prime
|
||||
* sur la demande d'effacement (art. 17.3.b). D'où deux traitements distincts :
|
||||
*
|
||||
* - **Effacé** : ce qui n'est conservé que pour le service. Disparaît.
|
||||
* - **Anonymisé** : ce qui doit être conservé, mais peut l'être sans rattachement
|
||||
* à une personne. Les pièces comptables gardent leur valeur probante sans
|
||||
* l'identité du demandeur.
|
||||
*
|
||||
* Une donnée anonymisée n'est plus une donnée personnelle : la conserver
|
||||
* ensuite ne relève plus du RGPD. C'est ce qui rend l'arbitrage tenable.
|
||||
*/
|
||||
|
||||
export interface RetentionRule {
|
||||
/** Table concernée. */
|
||||
table: string;
|
||||
/** Ce que devient la donnée à la demande d'effacement. */
|
||||
onErasure: 'delete' | 'anonymise' | 'keep';
|
||||
/**
|
||||
* Durée de conservation en mois, `null` si liée à la vie du compte.
|
||||
*
|
||||
* À ne pas confondre avec `onErasure` : celui-ci décrit la réponse à une
|
||||
* demande de la personne, celle-ci la limite au-delà de laquelle la donnée
|
||||
* n'a plus de raison d'être conservée, même sans demande (art. 5.1.e).
|
||||
*/
|
||||
months: number | null;
|
||||
/**
|
||||
* Colonne horodatée qui fait courir le délai. `null` lorsque la durée est
|
||||
* liée à la vie du compte : il n'y a alors rien à purger dans le temps.
|
||||
*/
|
||||
timestampColumn: string | null;
|
||||
/**
|
||||
* Condition SQL désignant les lignes que la purge ne doit jamais emporter,
|
||||
* même une fois le délai écoulé. `null` quand toute la table suit la règle.
|
||||
*/
|
||||
keepWhere: string | null;
|
||||
/** Pourquoi cette durée — la justification attendue par l'art. 30. */
|
||||
basis: string;
|
||||
}
|
||||
|
||||
export const RETENTION_RULES: readonly RetentionRule[] = [
|
||||
{
|
||||
table: 'users',
|
||||
keepWhere: null,
|
||||
timestampColumn: null,
|
||||
onErasure: 'anonymise',
|
||||
months: null,
|
||||
basis:
|
||||
"Le compte est anonymisé plutôt que supprimé : les réservations y font référence et doivent rester rattachables à une pièce comptable, sans l'identité de la personne.",
|
||||
},
|
||||
{
|
||||
table: 'csv_bookings',
|
||||
keepWhere: null,
|
||||
timestampColumn: 'created_at',
|
||||
onErasure: 'anonymise',
|
||||
months: 120,
|
||||
basis:
|
||||
'Pièce commerciale et comptable. Le code de commerce français impose dix ans de conservation des documents comptables (art. L123-22).',
|
||||
},
|
||||
{
|
||||
table: 'audit_logs',
|
||||
keepWhere: "action NOT LIKE 'gdpr\\_%'",
|
||||
timestampColumn: 'timestamp',
|
||||
onErasure: 'anonymise',
|
||||
months: 12,
|
||||
basis:
|
||||
"Journal de sécurité : nécessaire à la détection d'accès illégitimes (art. 32), et attendu par la CNIL avec une durée de six mois à un an.",
|
||||
},
|
||||
{
|
||||
table: 'notifications',
|
||||
keepWhere: null,
|
||||
timestampColumn: 'created_at',
|
||||
onErasure: 'delete',
|
||||
months: 12,
|
||||
basis: "Confort de service, sans valeur probante : rien ne justifie de les conserver.",
|
||||
},
|
||||
{
|
||||
table: 'trade_conversations',
|
||||
keepWhere: null,
|
||||
timestampColumn: 'updated_at',
|
||||
onErasure: 'delete',
|
||||
months: 12,
|
||||
basis:
|
||||
"Échanges avec l'assistant IA. Conservés pour que la personne retrouve ses conversations, sans obligation légale : ils s'effacent à la demande.",
|
||||
},
|
||||
{
|
||||
table: 'api_keys',
|
||||
keepWhere: null,
|
||||
timestampColumn: null,
|
||||
onErasure: 'delete',
|
||||
months: null,
|
||||
basis: "Moyen d'accès : il disparaît avec le compte.",
|
||||
},
|
||||
{
|
||||
table: 'cookie_consents',
|
||||
keepWhere: null,
|
||||
timestampColumn: 'consent_date',
|
||||
onErasure: 'delete',
|
||||
months: 13,
|
||||
basis:
|
||||
'Preuve du consentement (art. 7.1). La CNIL recommande de conserver cette preuve tant que le consentement est valable, soit treize mois.',
|
||||
},
|
||||
];
|
||||
|
||||
/** Règle dont le délai peut être appliqué dans le temps, sans demande. */
|
||||
export interface PurgeableRule extends RetentionRule {
|
||||
months: number;
|
||||
timestampColumn: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Règles que la purge périodique peut appliquer.
|
||||
*
|
||||
* Les tables dont la durée est liée à la vie du compte en sont exclues : leur
|
||||
* point de départ n'est pas une date en base, mais la fermeture du compte, que
|
||||
* l'effacement traite déjà.
|
||||
*/
|
||||
export function purgeableRules(rules: readonly RetentionRule[] = RETENTION_RULES): PurgeableRule[] {
|
||||
return rules.filter(
|
||||
(rule): rule is PurgeableRule => rule.months !== null && rule.timestampColumn !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les noms de table et de colonne sont interpolés dans du SQL — un paramètre
|
||||
* lié ne peut pas porter un identifiant. Ils viennent de constantes, mais le
|
||||
* jour où une règle sera renseignée depuis une configuration, cette barrière
|
||||
* sera déjà là.
|
||||
*/
|
||||
const SAFE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
||||
|
||||
export function assertSafeIdentifier(value: string): string {
|
||||
if (!SAFE_IDENTIFIER.test(value)) {
|
||||
throw new Error(`Identifiant SQL refusé par la politique de conservation : ${value}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Valeur substituée aux données identifiantes lors d'une anonymisation. */
|
||||
export const ANONYMISED = 'anonymised';
|
||||
|
||||
/**
|
||||
* Adresse de remplacement d'un compte effacé.
|
||||
*
|
||||
* Unique par compte : la colonne `email` porte une contrainte d'unicité, et
|
||||
* deux effacements successifs échoueraient sur une valeur constante. Elle ne
|
||||
* permet aucun rattachement — l'identifiant technique existait déjà en base.
|
||||
*/
|
||||
export const anonymisedEmail = (userId: string): string => `${ANONYMISED}+${userId}@invalid.local`;
|
||||
Loading…
Reference in New Issue
Block a user