diff --git a/apps/frontend/src/lib/legal/processing.ts b/apps/frontend/src/lib/legal/processing.ts new file mode 100644 index 0000000..7cd49df --- /dev/null +++ b/apps/frontend/src/lib/legal/processing.ts @@ -0,0 +1,113 @@ +/** + * Registre des traitements — source unique. + * + * ⚠️ Contenu juridique. Ce fichier liste les destinataires des données et ce + * que le site dépose sur l'appareil du visiteur. La politique de + * confidentialité publique, la politique de cookies et la page de conformité + * de l'administration le lisent toutes les trois : un sous-traitant ajouté ici + * apparaît partout, et il devient impossible de mettre à jour l'un en oubliant + * les autres. Il tient lieu de registre de l'article 30 côté produit. + * + * Seuls les faits structurels vivent ici — noms, nature, encadrement du + * transfert. Les descriptions sont dans les fichiers de messages, sinon la + * moitié de la page resterait en français pour un lecteur anglophone. + * + * Les durées de conservation, elles, vivent côté serveur + * (`domain/services/data-retention`) : c'est le serveur qui les applique. + */ + +/** Fondement d'un transfert hors Union européenne (chap. V du RGPD). */ +export type TransferBasis = + | 'eu' // Aucune sortie de l'Union. + | 'adequacy' // Décision d'adéquation de la Commission. + | 'scc'; // Clauses contractuelles types. + +export interface Subprocessor { + /** Clé de traduction sous `marketing.privacy.processors`. */ + key: string; + /** Nom commercial, identique dans toutes les langues. */ + name: string; + transferBasis: TransferBasis; + /** Vrai si le prestataire n'intervient que sur une fonctionnalité précise. */ + conditional: boolean; +} + +/** + * Chaque entrée correspond à un service réellement branché, vérifié dans le + * code — pas une liste d'intentions. + */ +export const SUBPROCESSORS: readonly Subprocessor[] = [ + { key: 'stripe', name: 'Stripe', transferBasis: 'scc', conditional: false }, + { key: 'openai', name: 'OpenAI', transferBasis: 'scc', conditional: true }, + { key: 'sentry', name: 'Sentry', transferBasis: 'scc', conditional: false }, + { key: 'pappers', name: 'Pappers', transferBasis: 'eu', conditional: true }, + { key: 'oauth', name: 'Google, Microsoft', transferBasis: 'scc', conditional: true }, +]; + +export interface StoredItem { + /** Nom technique, tel qu'il apparaît dans le navigateur. */ + name: string; + /** Cookie déposé par le serveur, ou valeur écrite par la page. */ + kind: 'cookie' | 'localStorage'; +} + +/** + * Ce que le site dépose réellement. + * + * L'inventaire précédent annonçait Google Analytics (`_ga`, `_gid`, `_gat`), + * Facebook Pixel (`_fbp`), LinkedIn (`li_fat_id`) et HubSpot (`hubspotutk`). + * Aucun de ces scripts n'est chargé par l'application : décrire des traceurs + * qui n'existent pas est aussi inexact que d'en dissimuler, et rendait sans + * objet le consentement demandé pour eux. + */ +export const ESSENTIAL_STORAGE: readonly StoredItem[] = [ + { name: 'accessToken', kind: 'cookie' }, + { name: 'refreshToken', kind: 'cookie' }, + { name: 'xpeditis_session', kind: 'cookie' }, + { name: 'cookieConsent', kind: 'localStorage' }, +]; + +export const FUNCTIONAL_STORAGE: readonly StoredItem[] = [{ name: 'NEXT_LOCALE', kind: 'cookie' }]; + +/** + * Responsable du traitement (art. 13.1.a). + * + * Les champs à `null` sont obligatoires et manquants. La page publique ne + * publie que ce qui est renseigné, et la console d'administration les signale : + * mieux vaut une mention absente, visible et corrigeable, qu'une mention + * inventée. + */ +export interface ControllerIdentity { + tradingName: string; + legalName: string | null; + registrationNumber: string | null; + address: string | null; + privacyContact: string; + /** Délégué à la protection des données, si un DPO a été désigné. */ + dpo: string | null; +} + +export const CONTROLLER: ControllerIdentity = { + tradingName: 'Xpeditis', + legalName: null, + registrationNumber: null, + address: null, + privacyContact: 'privacy@xpeditis.com', + dpo: null, +}; + +/** Autorité de contrôle compétente, à mentionner au titre de l'art. 13.2.d. */ +export const SUPERVISORY_AUTHORITY = { + name: 'Commission nationale de l’informatique et des libertés (CNIL)', + url: 'https://www.cnil.fr/fr/plaintes', +} as const; + +/** Mentions obligatoires encore absentes de l'identité du responsable. */ +export function missingControllerFields(controller: ControllerIdentity = CONTROLLER): string[] { + const required: [keyof ControllerIdentity, string][] = [ + ['legalName', 'legalName'], + ['registrationNumber', 'registrationNumber'], + ['address', 'address'], + ]; + return required.filter(([field]) => !controller[field]).map(([, key]) => key); +}