/** * 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); }