Suivez l'évolution de Boilerplate-Stack avec toutes nos mises à jour, nouvelles fonctionnalités et améliorations.
v1.35Minor
Routage LLM ZDR centralisé avec bascule résiliente, intégrité des commissions d'affiliation, limites d'API par route et garde-fous qualité exécutables
Chaque requête de conversation et d'embedding en production franchit désormais une frontière LlmClient unique, côté serveur uniquement, avec des charges utiles sans conservation de données, un catalogue de modèles revu, un disjoncteur partagé et une seule bascule inter-fournisseurs qui ne se déclenche jamais après le premier jeton diffusé. L'attribution d'affiliation passe à un horodatage de clic signé en HMAC et à une éligibilité aux commissions adossée aux paiements, les appels aux services externes reçoivent des délais bornés et annulables, et les compteurs de limitation sont désormais cloisonnés par méthode HTTP et par route canonique. Les audits qualité du dépôt sont de nouveau exécutables et vérifiés par la CI à chaque push, et deux vulnérabilités transitives de sévérité haute ont été corrigées par des surcharges étroitement ciblées plutôt que par des correctifs forcés.
Routage LLM ZDR centralisé. Chaque requête de conversation et d'embedding en production franchit désormais une frontière LlmClient unique, côté serveur uniquement. Les opérateurs choisissent les API directes OpenAI, Anthropic et Google ou OpenRouter via AI_LLM_TRANSPORT ; les requêtes OpenRouter exigent toujours des points de terminaison compatibles ZDR, refusent la collecte de données par le fournisseur, imposent la prise en charge des paramètres et autorisent la bascule de fournisseur au niveau de la passerelle. Les requêtes OpenAI directes fixent store: false et l'indicateur ZDR du SDK, tandis que la documentation d'exploitation précise que le ZDR contractuel chez un fournisseur direct reste un contrôle relevant du compte et du DPA
Le catalogue de modèles texte sélectionnables a été revu le 18 août 2026 par rapport aux catalogues officiels des fournisseurs et à l'API de modèles en direct d'OpenRouter. Il utilise désormais GPT-5.6 Sol/Terra/Luna, Claude Opus 5/Sonnet 5/Haiku 4.5 et les modèles Gemini 3.6/3.5/3.1/2.5 actuels ; GPT-4, les anciens Claude, Gemini 2.0 (arrêté) et les points de terminaison de génération d'images ont été retirés de la sélection de conversation. Claude Fable 5 est délibérément exclu, Anthropic documentant une conservation obligatoire de 30 jours et aucune éligibilité ZDR. Les limites de contexte, les tarifs directs et OpenRouter, les seuils de cache, les valeurs par défaut, les identifiants de modèles et la compatibilité d'échantillonnage relèvent de la configuration ; les appels GPT-5.6, Claude Opus 5/Sonnet 5, Gemini 3.6 Flash et Gemini 3.5 Flash-Lite omettent les paramètres de température obsolètes ou non pris en charge
Les routes LLM partagent désormais un disjoncteur adossé à la connexion Upstash Redis existante en production et à un backend déterministe local au processus en développement et dans les tests. Cinq échecs transitoires en 30 secondes ouvrent une route pendant 30 secondes ; une unique sonde semi-ouverte atomique, avec bail, peut la refermer ou la rouvrir. Les défaillances Redis sont limitées en fréquence, expurgées et échouent en mode ouvert, afin que le magasin d'observabilité ne puisse pas provoquer une panne globale de l'IA
Les fournisseurs directs bénéficient d'une seule nouvelle tentative annulable avec gigue ; OpenRouter n'en reçoit aucune au niveau applicatif, puisqu'il assure déjà le routage entre fournisseurs. Un échec transitoire survenu avant toute sortie peut recourir à une seule bascule inter-fournisseurs revue, y compris lorsque le circuit principal est ouvert. La bascule ne franchit jamais la liste d'autorisation d'un agent, ne s'exécute jamais après le premier jeton diffusé et ne change jamais de modèle d'embedding, car des espaces vectoriels incompatibles corrompraient la similarité RAG
Les lignes d'audit de succès et d'erreur consignent désormais le modèle demandé et le modèle résolu, le fournisseur, la route, l'identifiant de fournisseur et de requête en amont, l'état et le magasin du circuit, le résultat, la catégorie et la durée bornés de la tentative, ainsi que la survenue éventuelle d'une bascule. Les invites, réponses, corps de réponse du fournisseur, identités, secrets et erreurs brutes en sont exclus. Les crédits restent d'un jeton d'entrée/sortie rapporté par le fournisseur et sont débités une seule fois sur le modèle qui a réellement servi la réponse ; l'historique de conversation et les métadonnées SSE finales identifient de même le modèle résolu. Les résumés de tentatives bornés réutilisent la colonne JSONB ai_requests.metadata existante, si bien que le disjoncteur ne nécessite aucune migration supplémentaire
La QA déterministe valide désormais les identifiants canoniques de modèles, les charges utiles ZDR obligatoires pour la conversation et les embeddings, les limites de nouvelles tentatives directes, la bascule automatique d'OpenAI vers Anthropic, les transitions de circuit ouvert et semi-ouvert, l'unicité atomique des sondes, le comportement de Redis en échec ouvert, la persistance de l'audit de bascule, les listes d'autorisation des agents et la règle d'absence de bascule après une sortie partielle. Le parcours navigateur privé provoque une défaillance de fournisseur et confirme que la même soumission utilisateur se poursuit sur la bascule configurée, avec exactement un débit et sans ligne d'erreur erronée
Les appels aux services externes sont bornés et récupérables. Les requêtes HTTP Brevo et Mailjet utilisent désormais le délai EMAIL_REQUEST_TIMEOUT_MS défini en configuration (5000 ms par défaut, borné entre 1000 et 30000 ms). Le délai interrompt réellement la requête réseau sous-jacente au lieu de simplement la concurrencer, si bien que les flux de contact, de newsletter et de facturation pris en charge peuvent basculer vers leur file d'attente e-mail durable existante lorsqu'un fournisseur cesse de répondre
Claude Code, Codex, Gemini, OpenCode, Cursor et les miroirs de règles générés appliquent désormais le même contrat de fiabilité à toute intégration de service externe : délais bornés et annulables, temporisation plafonnée réservée aux erreurs transitoires, idempotence avant toute nouvelle tentative d'écriture, et file d'attente en base de données ou outbox transactionnelle lorsqu'un effet de bord doit survivre à l'échec d'une requête ou d'un processus. La règle impose également des réservations atomiques, la reprise des travaux bloqués, un nombre borné de tentatives, une visibilité opérationnelle en fin de course, des erreurs expurgées et des tests ciblés de délai, de reprise et de rejeu
Les cookies d'affiliation sont désormais consommés sur le compte de facturation vérifié avant la création du Checkout Stripe, avec un repli à l'onboarding pour les flux hérités. Les candidatures conservent une preuve d'acceptation explicite des conditions, l'approbation crée le premier lien de manière atomique, et les affiliés peuvent créer des liens bornés de même origine après avoir réaccepté les conditions en vigueur
Le cookie d'affiliation porte désormais un horodatage de clic authentifié par HMAC : les fenêtres d'attribution Starter et Partner démarrent au clic réel plutôt qu'au paiement, et un cookie non signé ne peut plus fabriquer une fenêtre neuve. Cela invalide volontairement les anciens cookies en clair au déploiement : les visiteurs sans attribution persistée doivent repasser par une URL d'affiliation, tandis que les attributions déjà enregistrées en base restent valides. Un abonnement converti pour la première fois à l'intérieur de cette fenêtre reste éligible aux commissions jusqu'au plafond configuré de 12/24 factures ; la même attribution expirée continue de refuser une nouvelle conversion d'abonnement, de licence ou de pack de crédits
Les plafonds par IP sont verrouillés transactionnellement, l'auto-attribution couvre le chevauchement d'appartenances, et les empreintes anti-fraude ne sont plus lisibles via la Data API authentifiée. L'ordre des factures d'abonnement positives provient des lignes de paiement canoniques sous verrou d'abonnement ; les factures de valeur nulle, de prorata, remboursées, échouées ou contestées ne consomment pas d'éligibilité aux commissions. Les rejeux de conversion sont vérifiés en conflit, les périodes de rétention sont figées, et un remboursement ou un litige perdu n'annule que la facture concernée
Les statistiques de commissions restent séparées par devise de facturation d'origine. La configuration des versements est manuelle uniquement ; Stripe Connect est volontairement exclu. Les conditions renvoient désormais à un document versionné dont l'identifiant immuable est conservé ; une acceptation périmée désactive les liens, les attributions et les nouvelles commissions. Une attribution tardive réconcilie les paiements positifs antérieurs, les clics bruts font l'objet d'une purge de rétention, et les mutations d'administration sont journalisées atomiquement. L'historique de migration supprimé par accident a été restauré, la migration d'affiliation est reflétée dans le schéma complet, et une migration correctrice distincte réconcilie l'écart antérieur entre schéma et migrations
Les garde-fous qualité sont de nouveau exécutables. L'audit de dépendances n'exécute plus la configuration TypeScript de Playwright pendant la construction de son graphe, si bien que les alias de chemins ne peuvent plus empêcher Knip de démarrer. Ses espaces de travail racine et Remotion sont désormais décrits explicitement, les dépendances d'outillage implicites sont documentées de façon restreinte, et les dépendances et alias réellement inutilisés ont été supprimés. Les cinq cycles tarification/e-mail relevés par Dependency Cruiser ont été éliminés en déplaçant les primitives partagées de devise et de formulation de notification vers des modules sans dépendances
L'audit de traductions vérifie désormais la parité des catalogues et la validité des traductions, ce qui correspond au traducteur maison du projet ; l'heuristique d'usage dans les sources, non prise en charge, qui signalait à tort des milliers de clés inutilisées, a été retirée. La CI exécute désormais les audits de traduction, de dépendances et d'imports circulaires à chaque push et à chaque pull request
Toute réponse d'API 500 explicite doit désormais être précédée d'un journal d'erreur structuré ; l'audit échoue globalement au lieu de se contenter d'un avertissement pour les routes héritées. Le seuil de vulnérabilité des dépendances a été relevé à low, la version corrigée d'esbuild est imposée dans tout l'espace de travail et la chaîne ESLint Remotion obsolète a été mise à niveau, laissant l'audit à zéro vulnérabilité connue. La version du boilerplate est lue depuis package.json par l'assistant d'initialisation et alignée sur la dernière version documentée, 1.35.0
Limites d'API cloisonnées par route. Les clés de limitation incluent désormais une méthode HTTP versionnée et une portée de route canonique avant l'identité de l'appelant et l'IP finale. Des points de terminaison et des méthodes sans rapport ne consomment plus le quota les uns des autres, les chaînes de requête n'influencent jamais le compteur, et chaque gestionnaire dynamique déclare un gabarit de route stable afin que des UUID, slugs ou codes variables ne puissent pas contourner la limitation. L'audit des motifs d'API rejette désormais toute future route dynamique dépourvue de cette déclaration. Les limiteurs intégrés à l'IA, à Stripe et à l'exécution de tâches à double authentification utilisent le même constructeur de clés ; la branche administrateur de l'exécuteur de tâches s'authentifie désormais avant d'appliquer son budget utilisateur et IP
Les lectures d'administration plateforme disposent désormais d'un palier dédié de 120/min/route et les mutations ordinaires de 30/min/route. Les opérations d'accès, de facturation, de rôles, de cron, de contre-passation et les opérations destructrices conservent explicitement le palier strict de 5/min/route. Toutes les limites, y compris les budgets par destinataire pour les liens magiques et les codes OTP, sont centralisées dans config/rate-limits.ts ; l'espace de noms Redis v2 évite les collisions pendant que les anciennes clés à fenêtre d'une minute expirent naturellement
Le blueprint de codage assisté par IA est aligné sur les contrats Supabase actuels. Claude, Codex, Gemini, OpenCode, Cursor et les miroirs générés font désormais passer le travail base de données par le modèle de propriété centré sur le compte, des habilitations Data API explicites accompagnées de RLS, la parité entre migrations et schéma complet, des privilèges SECURITY DEFINER sûrs, les frontières actuelles des clients Supabase et des recommandations MCP en lecture seule par défaut et à portée restreinte. Le AI Blueprint publié n'enseigne plus d'assistants retirés, d'accès distant non restreint ni de workflows CLI manquants
Les instructions frontend reposent désormais sur un unique contrat DESIGN.md à l'échelle du projet, mis en œuvre par app/globals.css, components.json, les primitives et motifs locaux, DM Sans et JetBrains Mono, des jetons oklch() sémantiques, Tailwind CSS 4 et le provider Motion existant. Le fichier de passation .impeccable.md antérieur à la v4 a été supprimé et le bundle Impeccable revu est passé de 4.0.4 à 4.1.1, ses chemins absents de CLI, d'auto-mise à jour et d'installation de hooks étant désactivés pour ce dépôt. Toute modification frontend commence désormais par le mobile et charge Impeccable ainsi que les seules recommandations React, shadcn, Tailwind, motion, accessibilité et performance réellement nécessaires à la tâche. Les règles et compétences restent canoniques sous .claude/, avec des miroirs multi-outils synchronisés et une validation automatisée des écarts
Playwright ne parvenait pas à démarrer son serveur web en CI. playwright.config.ts lançait l'application avec pnpm run dev -- --hostname 127.0.0.1, et la version de pnpm épinglée transmet -- tel quel : Next recevait donc --hostname comme répertoire de projet. Toutes les suites navigateur de la CI (test:e2e:api, test:e2e:auth, test:e2e:oauth, test:a11y, test:responsive) échouaient au démarrage du serveur. La configuration invoque désormais la CLI directement et dérive l'hôte et le port de l'URL de base QA validée, si bien que le serveur démarré est toujours celui que Playwright interroge
Le portail QA local signalait des échecs qu'il ne savait pas expliquer. currentStep n'était affecté qu'à l'intérieur du bloc de la phase 6, si bien que toutes les étapes antérieures étaient rapportées comme initialization, et la sortie de la CLI Supabase était capturée puis jetée. Le portail nomme désormais chaque étape, et QA_LOCAL_GATE_DIAGNOSTICS=true ajoute la raison de l'étape en échec ainsi que des extraits bornés et expurgés de stderr et stdout (la sortie de supabase status n'est jamais transmise)
Le portail contrôle également le jeu de migrations avant start et reset. La CLI ignore silencieusement tout fichier qui ne correspond pas à son motif horodaté, ainsi que tout <timestamp>_init.sql : une migration ignorée ne produit aucune sortie et ne se manifeste que plus tard, au moment du seed, par une erreur relation "public.app_settings" does not exist. classifyMigrationFileNames reproduit ce filtre et échoue avant tout téléchargement d'image ; son message ne nécessite aucun indicateur de diagnostic, puisqu'il est construit uniquement à partir de noms de fichiers du dépôt
Deux vulnérabilités transitives de sévérité haute ont été corrigées par des surcharges ciblées, sans nouvelle exclusion d'âge de publication ou de politique de confiance et sans correctif d'audit forcé. GHSA-2v37-7h3g-55p8 (nanoid) a été requalifiée de <3.3.17 à <3.3.18 : le plancher existant postcss>nanoid: ^3.3.17 a donc cessé silencieusement de la couvrir — la surcharge paraissait correcte alors que le lockfile restait sur la version vulnérable 3.3.17. Relever le plancher à ^3.3.18 corrige le problème ; nanoid ne se résout qu'en une seule version et les 52 chemins passent tous par postcss, si bien que la portée existante couvre encore tout l'arbre, et 3.3.18 franchit d'elle-même la quarantaine de sept jours
GHSA-jmr9-qjv8-65gv (extract-zip) n'offrait aucun chemin de mise à niveau : l'avis désigne >=2.0.2 comme corrigée, mais cette version n'a jamais été publiée — la 2.0.1 de 2020 reste la plus récente et le paquet est de fait abandonné. Il n'arrivait chez nous que par @lhci/cli > lighthouse > puppeteer-core, chaque maillon de cette chaîne étant épinglé à l'exact, si bien que rien en amont ne pouvait évoluer de lui-même. @puppeteer/[email protected] a remplacé extract-zip par modern-tar : la nouvelle surcharge ciblée puppeteer-core>@puppeteer/browsers: ^3.1.0 supprime donc purement et simplement le paquet vulnérable au lieu de contourner le problème par un épinglage. Le choix de ^3.1.0 plutôt que de ^3.2.0, plus récente, est délibéré : la 3.2.0 aurait exigé une entrée minimumReleaseAgeExclude, alors que la 3.1.0 élimine tout autant extract-zip et franchit la quarantaine sans aide
Le franchissement de cette version majeure a été vérifié plutôt que supposé. La 3.x exporte encore tous les symboles importés par puppeteer-core@24 et, bien qu'elle soit uniquement ESM là où la 2.13.2 était double, Lighthouse 12 est en ESM et résout la version ESM de puppeteer-core : le chemin consommateur ne passe donc jamais par require, et l'interopérabilité CJS a de toute façon été confirmée sur la version de Node épinglée. Le bilan net est une réduction de 22 paquets, [email protected] et [email protected] restant à version inchangée, si bien que les planchers d'assertion Lighthouse ne sont pas affectés. Les signatures ont été revérifiées sur 1658 paquets ; la vérification de types, 640 tests unitaires, les garde-fous, le lint et un build de production complet passent tous
v1.34Minor
Récompenses de parrainage adossées à un paiement, mode maintenance audité, automatisation QA jetable et webhooks Stripe Dahlia
Les récompenses de parrainage sont désormais adossées à un paiement réel et finalisé du compte parrainé : un remboursement ou un litige n'annule plus que la récompense qu'il a effectivement financée, et rien d'autre. Les administrateurs de la plateforme peuvent activer le mode maintenance depuis le tableau de bord d'administration sans redéploiement, derrière une RPC auditée et protégée contre le CSRF. Une pile QA jetable, orientée comptes, verrouille désormais le dépôt de bout en bout — espace privé, organisation, administration plateforme et couverture non fonctionnelle — et a mis au jour une longue liste de défauts réels. Les webhooks Stripe passent à la version d'API 2026-07-29.dahlia, et l'onglet Paiements de l'administration, qui n'avait jamais affiché la moindre ligne depuis sa mise en production, interroge enfin des colonnes qui existent.
Les récompenses de parrainage payantes exigent désormais un paiement local positif et finalisé appartenant au compte parrainé. Le parrainage conserve ce paiement dans qualifying_payment_id ; les récompenses liées à un abonnement exigent en plus une preuve d'abonnement Stripe persistée. Les remboursements intégraux et les litiges perdus n'annulent que la récompense qualifiée par le paiement concerné, si bien qu'un achat sans rapport ne peut plus déclencher de reprise. Des verrous consultatifs à la portée du compte ferment la concurrence entre webhook et attribution, et les paiements de valeur nulle, intégralement remboursés, absents ou rattachés à un autre compte sont rejetés
Les routes de paiement ne consomment le cookie de parrainage qu'après l'autorisation de facturation et avant de renvoyer l'URL Stripe, en utilisant exactement le compte de facturation vérifié. Le repli d'onboarding, désormais attendu, résout un espace de travail détenu en mode B2B ou le compte personnel en mode B2C, et peut réconcilier un paiement déjà validé. L'application manuelle d'un code ne peut pas réutiliser un paiement historique. L'invalidation de cache est ciblée grâce à l'identifiant du compte parrain renvoyé par les nouvelles RPC de qualification et d'annulation
La migration du parrainage est répercutée dans supabase/schema.sql et dans les types de base de données générés. Une suite pgTAP de 31 assertions couvre la validation des paiements, l'annulation exacte, la réconciliation d'un paiement antérieur à l'attribution, la preuve réservée aux abonnements, les privilèges des RPC et les index ; la barrière ciblée fonctionnalité/navigateur couvre les mutations de parrainage côté membre et administrateur, ainsi que les états responsives et d'accessibilité
Mode maintenance audité au niveau de la plateforme. Les administrateurs peuvent désormais activer ou désactiver la maintenance depuis /admin-dashboard/settings sans modifier de variable d'environnement ni redéployer. La mutation d'administration — booléen strict, protégée contre le CSRF, limitée en débit et exigeant une authentification récente — délègue à une RPC SECURITY DEFINER réservée aux utilisateurs authentifiés, qui répète le contrôle is_admin et enregistre atomiquement le changement dans admin_logs
Lorsque la maintenance est active, les pages localisées du site et /docs redirigent temporairement vers le nouvel écran localisé /[locale]/maintenance, avec les protections no-store, Retry-After et noindex. Les routes d'administration et de récupération d'authentification restent accessibles, tandis que les API, les webhooks Stripe, les tâches de fond, les sondes de santé, les ressources statiques et le repli hors ligne continuent de fonctionner. Les requêtes de page autres que GET reçoivent une réponse 503 typée MAINTENANCE_MODE. La lecture scalaire du statut utilise un cache serveur Next.js partagé de 60 secondes avec un client de service Supabase compatible avec la mise en cache, s'exécute en parallèle de la résolution de session et échoue délibérément en mode ouvert, avec journalisation structurée, si Supabase est indisponible. L'invalidation immédiate est volontairement absente ; l'interface des réglages indique que l'activation comme le rétablissement peuvent être différés jusqu'au rafraîchissement du cache
La migration du mode maintenance positionne le drapeau à false par défaut, n'expose qu'une RPC publique de lecture booléenne étroite, et est répercutée dans supabase/schema.sql et dans les types de base de données générés. Le guide d'exploitation Production → Maintenance Mode documente le déploiement, les limites de récupération, les contrôles de sécurité, le comportement des requêtes et la vérification
Automatisation QA jetable et orientée comptes. Le dépôt épingle désormais la CLI Supabase et embarque une pile locale limitée à la boucle locale, un jeu de données QA idempotent et des suites pgTAP transactionnelles pour le schéma, les RLS et droits, ainsi que les invariants d'inscription et de compte. pnpm run qa:local:gate vérifie qu'il s'agit bien du projet local exact, le réinitialise et le teste, provisionne des acteurs membre, administrateur, propriétaire et administrateur plateforme via Supabase Admin Auth, exécute les contrôles d'authentification sans mot de passe, de rôles, d'isolation inter-locataires et d'accessibilité des tableaux de bord authentifiés, puis supprime l'état de stockage du navigateur et le volume de base de données jetable
La documentation publique comporte désormais une section dédiée Quality Assurance → QA Automations qui explique le cycle de vie de la barrière jetable, les profils de commandes ciblés, l'isolation des comptes et personas, les frontières avec les fournisseurs, les coupe-circuits de production, les manifestes de dérive de routes, les diagnostics et la démarche pour ajouter de la couverture QA
La couverture non fonctionnelle de la phase 6 est complète via pnpm run qa:local:phase6. Le profil jetable réservé à la production construit et vérifie les traces de Next.js, génère le worker Serwist, analyse 14 routes publiques dans les deux thèmes ainsi que docs/offline et l'ensemble des 58 destinations privées, organisation et administration détenues, exerce cinq points de rupture, compare huit références visuelles versionnées et exécute une matrice de 36 cas sur Chromium, Firefox et WebKit. Il prouve également le comportement au clavier, au zoom et en mouvement réduit, les frontières des scripts de consentement et du Global Privacy Control, les états manifeste, service worker, hors ligne et push, l'isolation du cache d'exécution entre API et documents, les budgets de performance publique et authentifiée ainsi que de requêtes PostgreSQL, 15 audits Lighthouse jugés sur la médiane et un test de charge borné sur la sonde de santé. La barrière a révélé et corrigé des libellés, contrastes et zones de défilement d'accessibilité, une sémantique d'onglets d'analytique invalide, le retrait du consentement côté script, le repli de navigation hors ligne, une mise en cache d'exécution par défaut dangereuse des réponses API, HTML et RSC dynamiques, la gestion locale de la CSP par WebKit, une sélection d'audit d'administration obsolète, un contraste transitoire du bouton de session, un LCP de héros masqué jusqu'à l'hydratation de Motion, des courses de test sur les redirections en flux et une génération de charge non bornée
La couverture privée de la phase 2 a démarré avec une commande ciblée pnpm run test:e2e:private. La suite authentifiée côté membre exerce désormais la vue d'ensemble localisée du tableau de bord privé et le sélecteur de packs de crédits, et valide l'intégralité du contrat d'export CSV RGPD : en-têtes de téléchargement et de sécurité, les 29 sections documentées dans l'ordre, une projection des clés d'API sans secret, et l'exclusion des identités appartenant à d'autres locataires
La suite privée pilote aussi la vraie route chat/SSE et la comptabilité Supabase face à un fournisseur compatible OpenAI, sans secret et limité à la boucle locale. Elle prouve la barrière préalable en 402, des débits de jetons fournisseur exacts, un historique utilisateur/assistant durable et le marquage des sorties d'IA, l'échec puis la reprise du fournisseur sans faux succès ni prompt dupliqué, une interruption brutale de socket suivie d'une reprise propre, le rejet d'une double soumission dans le même tick avec une seule session, une seule requête et un seul débit, ainsi que l'annulation sans facturation. Les flux amont bloqués s'interrompent désormais au plafond aiConfig.streamTimeoutMs porté par la configuration et entrent dans le chemin d'erreur observable à zéro jeton. Le client conserve les tours d'assistant interrompus sous forme d'erreurs localisées et rejouables, tandis que des garde-fous synchrones à vol unique empêchent la duplication de sessions et de flux avant que l'état React ne suive. L'implémentation corrige aussi les défauts mis au jour par cette preuve : les exceptions fournisseur converties par BaseAgent ne peuvent plus passer pour des requêtes réussies à zéro jeton, les insertions groupées de messages de chat via PostgREST donnent désormais aux deux lignes un ensemble de colonnes identique afin que la contrainte NOT NULL sur ai_generated ne puisse pas annuler l'historique, et le cookie de compte actif du tableau de bord privé ne sélectionne plus qu'une adhésion validée tout en gardant alignés tous les champs liés au compte. La résolution d'accès hybride préserve cet ordre de comptes validés entre les sources abonnement et licence, et les alertes de quota fournisseur expurgent les secrets avant l'envoi des e-mails. pnpm run qa:local:private exécute ce profil jetable ciblé ; seule la barrière locale injecte son marqueur exact de fournisseur déterministe, si bien que les commandes génériques privé/persona excluent toutes les spécifications adossées à un fournisseur au lieu de risquer les véritables identifiants d'un développeur
Le même profil fournisseur fail-closed couvre désormais l'intégralité du cycle de vie documents/RAG. Les téléversements exigent un couple MIME/extension exact et des nombres magiques, maintiennent la limite de 10 Mo distincte d'une surcharge multipart bornée, accusent réception du travail asynchrone par 202 Accepted et Location, et terminent explicitement la réponse navigateur avant toute interrogation. Les embeddings utilisent un OPENAI_BASE_URL validé et réservé au serveur, un plafond de 60 secondes par requête, des lots multi-entrées bornés et des contrôles stricts d'index et de dimension sur la réponse du fournisseur. Les jetons rapportés par le fournisseur sont persistés dans un champ de liste scalaire et pilotent le rafraîchissement des crédits dans l'interface, au lieu du nombre de fragments. La suppression retire l'objet Storage privé avant la ligne du document et s'appuie sur la cascade de la base pour les fragments. Le contrat navigateur déterministe prouve le refus pour un compte tiers, un débit de jetons exact, l'état prêt et le détail, ainsi qu'un nettoyage complet de la base, des fragments et de l'objet ; les commandes génériques privé/persona excluent également cette spécification adossée à un fournisseur
La couverture privée locale de la phase 2 est désormais complète. Un profil fail-closed avec fonctionnalités activées exerce la création et le partage d'un code de parrainage personnel, l'attribution, la qualification et les récompenses atomiques, puis la candidature d'affiliation, les états en attente et approuvé, l'attribution par lien, l'enregistrement des conversions et les métriques localisées. Le contrat des réglages de compte, sans fournisseur, persiste les changements de profil et de consentement et prouve la planification de suppression ainsi que son annulation ; les contrats existants de session et d'export RGPD restent dans la même barrière. Les cinq pages privées reçoivent des analyses WCAG 2.2 A/AA et des contrôles de débordement horizontal à 320, 768 et 1280 pixels. Les tests ont révélé et corrigé un état d'affiliation obsolète après candidature, un défilement des conversions inaccessible au clavier, un repère main public dupliqué et l'absence de statut de chargement accessible sur les documents. La matrice responsive redémarre l'application pour obtenir une fenêtre de limitation de débit équivalente à la production, plutôt que d'attendre ou d'affaiblir la limite
La couverture du tableau de bord d'organisation (phase 3) est complète via pnpm run qa:local:org. La matrice jetable propriétaire/administrateur/membre exerce les neuf contrats de routes, le renvoi et la révocation d'invitations, les changements de rôle, les limites de retrait par un gestionnaire, l'analytique et la facturation isolées par compte, la création et la révocation de clés d'API à usage unique, la normalisation des réglages et la suppression réservée au propriétaire, puis les contrôles WCAG 2.2 A/AA et de débordement à 320, 768 et 1280 px sur les huit destinations rendues, avant de rejouer les contrats d'API authentifiée et de ressources dynamiques. L'implémentation déduit désormais l'adhésion du gestionnaire courant sans requête supplémentaire, réserve la facturation et les actions destructrices au propriétaire, ne conserve les droits de retrait des administrateurs que pour les membres ordinaires, rafraîchit l'état des Server Components sans rechargement complet, étiquette correctement les paiements terminés et nomme les contrôles clavier et icônes pour les technologies d'assistance
La couverture de l'administration plateforme (phase 4) est complète via pnpm run qa:local:admin. La matrice jetable rend les 43 pages d'administration (listes, création, détail, édition) avec des identifiants appartenant au worker, exerce les 36 familles d'API d'administration sur les cas nominaux de CRUD et d'actions, les entrées malformées et les ressources inconnues, et prouve chaque refus de page et d'API pour les personas membre, administrateur d'espace de travail et propriétaire d'espace de travail, journaux, parrainages et affiliations étant activés de façon hermétique. Les actions utilisateur couvrent désormais l'envoi de configuration après réauthentification récente, les changements de profil, de privilèges et de désactivation, les écritures atomiques au grand livre des crédits, la suppression protégée et les lignes d'audit ; les contrats du domaine abonnement alignent les appels Stripe, le miroir local et les événements d'audit sans contacter Stripe dans la barrière locale. La suite prouve aussi l'expurgation des secrets dans les journaux d'erreur et le hachage salé des adresses IP. Les défauts trouvés par la barrière et la revue de sécurité ont été corrigés : les RPC de crédits d'administration restent liées au client Supabase, les corps média malformés renvoient 400, et la suppression d'un utilisateur ne peut plus supprimer implicitement un espace de travail ou un compte facturé en externe. La classification des références de paiement distingue les véritables identifiants Stripe des sentinelles d'idempotence internes free_init_*, sub_init_*, sub_trial_* et end_trial_*, de sorte que les utilisateurs gratuits restent supprimables sans risque
La couverture CRUD d'administration pilotée par le navigateur (phase 7) a démarré via pnpm run qa:local:phase7. Les parcours Playwright localisés actuels créent, persistent, activent ou modifient, puis suppriment des tâches, gestionnaires de tâches, catégories de blog et étiquettes localisées appartenant au worker, à travers les vrais formulaires, menus, boîtes de confirmation, routes protégées contre le CSRF et contrôles de nettoyage exact en base. Les parcours de taxonomie prouvent aussi le refus de suppression d'un élément utilisé. Le profil ciblé conserve le limiteur de débit strict de l'administration et attribue aux parcours indépendants des adresses IP client réservées à la QA plutôt que d'affaiblir le quota. La même spécification est câblée dans la barrière d'administration ciblée et dans la barrière jetable par défaut ; les pages CMS/blog, les médias et le cron restent suivis dans QATests.md
Les projets Playwright authentifiés restent optionnels et fail-closed. Les exécutions par persona exigent QA_PERSONA_SETUP=true ainsi qu'un manifeste orienté comptes validé par Zod ; la préparation vérifie chaque identité Auth, profil, rôle et role_slug d'adhésion, espace de travail et source d'accès facturé avant d'émettre un lien magique. Les projets privilégiés désactivent trace, captures d'écran et vidéo. Une fixture de diagnostic partagée n'enregistre que des tuples bornés route/compteur canoniques pour les erreurs navigateur et les échecs de même origine, si bien que les charges utiles de console, URL de jeton, cookies, en-têtes, corps et piles n'entrent jamais dans les rapports. Les échecs de requête sont rattachés à la génération de navigation de leur frame principale : seules les ressources d'une page remplacée sont ignorées, les échecs de la page courante restant bloquants. La CI ajoute la barrière jetable base de données/personas locale et répète l'analyse de secrets après l'exécution du navigateur
La barrière locale capture désormais le message de lien magique tel que l'application le rend, via une extension temporaire et fail-closed de EMAIL_PROVIDER=noop. Deux parcours Playwright sans exposition de secret connectent des personas préqualifiés avec l'OTP livré puis avec l'URL de callback exacte livrée ; cette dernière est échangée hors de l'instrumentation Playwright avant l'installation de cookies validés. L'enregistreur exige des origines application et Supabase en boucle locale ainsi qu'un mode développement non indexable, écrit atomiquement des messages bornés validés par Zod et ignore les identifiants capturés lors d'une nouvelle tentative. Les exécutions privilégiées n'utilisent aucun rapporteur persistant, évitent les étapes de saisie Playwright porteuses de secrets, lient l'application à la boucle locale et suppriment les répertoires temporaires exacts d'e-mails et de sortie à la fin ou lors d'une interruption gérée, avant l'analyse de secrets postérieure à la barrière. Les adresses e-mail d'authentification passent désormais par sanitizeEmail() après validation Zod, et celles que la sanitisation réécrirait de façon destructrice sont rejetées au lieu de viser une autre identité
La déconnexion dispose désormais de sa propre session de persona isolée, afin de ne pas invalider un projet ACL exécuté en parallèle. Le navigateur exerce le vrai menu de compte et la route /api/auth/logout protégée contre le CSRF, puis prouve que le cookie d'authentification a disparu, que /api/user/sessions renvoie une erreur d'autorisation et qu'un contexte neuf rejouant l'état de stockage antérieur à la déconnexion est redirigé vers la page de connexion. La préparation des sessions ne génère ce cinquième état qu'après la préqualification complète des quatre personas, garde tous les artefacts désactivés, et la barrière locale le supprime dans un bloc finally
La révocation de session est désormais une opération Auth confirmée par le serveur, et non une API purement auditée suivie d'un appel navigateur non vérifié. DELETE /api/user/sessions exécute signOut({ scope: 'others' }), signale les échecs Auth, préserve la session courante et enregistre séparément le résultat d'audit. La déconnexion ordinaire /api/auth/logout utilise explicitement scope: 'local' au lieu du comportement global par défaut de Supabase et ne signale ni ne journalise plus un succès avant qu'Auth n'ait réussi ; les interfaces de compte et d'invitation s'appuient sur cette unique mutation serveur au lieu d'émettre une seconde déconnexion globale. Deux états propriétaire indépendants supplémentaires prouvent que l'autre session est rejetée tandis que la session courante reste authentifiée. Le titre de la page indique désormais activité de session, car sa liste est l'historique récent des journaux d'accès et non un inventaire vivant de auth.sessions, et la journalisation d'accès réutilise la barrière de confiance Cloudflare centralisée tout en assainissant les chaînes d'en-têtes stockées
Le durcissement complémentaire rend explicites la preuve et la frontière de données. Le test de déconnexion vérifie désormais qu'une seconde session survit à scope: 'local' ; la révocation prouve un refus applicatif immédiat via getUser(), sans prétendre qu'un JWT déjà émis serait invalide auprès de l'API Data de Supabase avant expiration. Le provisionnement et la révocation de personas sont refusés pour une préproduction hébergée et acceptés uniquement sur la cible en boucle locale. La lecture et la révocation de sessions sont passées de la route vers core/auth, les lectures indépendantes s'exécutent en parallèle, et user_access_logs devient une piste d'audit écrite par le service : les droits et politiques d'INSERT authentifié ont été retirés dans une migration répercutée. L'endpoint /api/user/log-activity, inutilisé et sélectionnable par le client, ainsi que son appel d'onboarding en double, ont été supprimés ; les vraies mutations comme /api/user/onboarding journalisent côté serveur leur propre événement fixe, de sorte qu'un client ne peut plus falsifier un type d'événement d'audit via un relais à rôle de service. Enfin, l'enregistreur d'e-mails local est fail-closed en exécution de production Webpack comme Turbopack ; une assertion post-build rejette tout code d'enregistreur compilé ou tracé ainsi que les entrées d'hôtes temporaires externes
Les webhooks Stripe utilisent désormais la version d'API 2026-07-29.dahlia. Le SDK Stripe passe en 22.4.0 et le client applicatif comme la sonde de disponibilité de préproduction dérivent la version d'API du SDK. Les gestionnaires de webhooks de facture consomment les formes Dahlia invoice.parent.subscription_details.subscription et Invoice Payments à la place des champs de premier niveau supprimés invoice.subscription et invoice.payment_intent, ce qui préserve les enregistrements de paiement d'abonnement et les recharges mensuelles de crédits
La vérification de signature des webhooks accepte un STRIPE_WEBHOOK_SECRET_PREVIOUS optionnel pendant la procédure de bascule à double endpoint de Stripe. La sonde de disponibilité reconnaît les endpoints de remplacement suffixés par une requête, exige un épinglage explicite de la version Dahlia, contrôle la liste des événements requis et envoie un événement inerte signé pour confirmer que la route déployée accepte le secret actif configuré
L'audit des dépendances a également révélé GHSA-2v37-7h3g-55p8 dans [email protected] de la branche PostCSS. Un forçage étroit postcss>nanoid le porte à la version corrigée 3.3.17 sans élargir les exclusions d'âge de publication ni de politique de confiance du dépôt
L'onglet Paiements de l'administration n'avait jamais affiché la moindre ligne. Signalé par « j'ai payé une licence et il n'y a aucune ligne dans Paiements » — la licence était correcte, c'est l'onglet qui interrogeait des colonnes inexistantes. La page de détail utilisateur sélectionnait amount_cents et credits_amount sur payments, alors que la table possède amount (entier, en centimes) et metadata jsonb ; aucune de ces deux chaînes n'apparaît dans supabase/schema.sql, et les seules occurrences dans tout le dépôt étaient ces trois lignes. PostgREST rejette la projection entière dès qu'une colonne est inconnue : la lecture renvoyait null, l'appel ne déstructurait que { data } en écartant l'erreur, payments retombait sur [], et l'onglet affichait son état vide « aucun paiement » prévu par la conception. Ni spécifique aux licences, ni récent — cet onglet était vide pour tous les utilisateurs et tous les types de paiement depuis sa mise en production. La lecture jumelle de org-dashboard/billing avait toujours utilisé amount correctement, si bien que les deux pages étaient en désaccord sur le schéma sans qu'aucune barrière ne puisse s'en apercevoir
Une requête en échec qui retombe sur un état vide prévu par la conception est indiscernable d'un vide réel, et c'est précisément ce qui a offert à ce bug sa longévité : un onglet Paiements vide est exactement ce que devrait voir un utilisateur qui n'a jamais payé, donc rien ne paraissait anormal tant que personne ne savait avec certitude qu'un paiement existait. Même forme que l'anti-pattern D9 — un repli silencieux dont la sortie est assez plausible pour que personne n'y lise un échec. L'erreur de requête part désormais vers logError (sans attente, catégorie admin, événement user_detail_payments_query_failed), de sorte qu'une projection rejetée apparaît dans /admin-dashboard/logs au lieu de se présenter comme une absence de données
Les crédits reproduisaient deux fois la même erreur : il n'existe pas non plus de colonne credits_amount, et les deux écrivains ne s'accordent pas sur la clé — les packs de crédits écrivent metadata.credits_amount tandis que les licences écrivent metadata.credits_included (core/billing/mutations.ts). Un nouvel utilitaire getPaymentCredits() lit les deux et les convertit, si bien qu'une licence accordant credits.oneTime affiche désormais son octroi au lieu d'un tiret. Un second défaut était masqué par le premier : license est une valeur de l'énumération payment_type et applyLicensePurchase l'écrit, mais getPaymentTypeLabel() n'avait aucune entrée correspondante — le badge aurait affiché le slug brut dès l'apparition des lignes. admin.userDetail.paymentTypeLicense a été ajouté aux quatre catalogues
v1.33Major
Passe de sécurité, refonte du design system, conformité au règlement européen sur l'IA et chaîne d'approvisionnement 100 % pnpm
La plus grosse version à ce jour : correction d'un contournement du sanitizer DOMPurify et nouvelle politique de chaîne d'approvisionnement 100 % pnpm, audit externe de la surface d'attaque qui referme onze constats (dont un callback d'authentification totalement hors périmètre d'audit), refonte du shell des tableaux de bord et du design system sur ~84 pages, information et marquage des sorties au titre de l'article 50 du règlement européen sur l'IA, comblement des lacunes RGPD sur l'export et la traçabilité du consentement, et une longue liste de correctifs d'accessibilité, d'i18n et de parcours d'invitation — dont les invitations d'espace de travail, qui n'avaient jamais pu être acceptées une seule fois.
Correction d'un contournement du sanitizer DOMPurify (GHSA-55q2-fjhq-7xh7) : retirer un hook IN_PLACE laisse un sous-arbre détaché exécutable. DOMPurify sous-tend HtmlRenderer, le rendu des shortcodes et markdownToHtml, il s'agissait donc d'un vecteur XSS réel et non d'une note de bas de page transitive. dompurify est désormais forcé en 3.4.13, avec une portée limitée à son unique consommateur (isomorphic-dompurify>dompurify) plutôt qu'appliquée à tout le graphe, et une exception par version exacte à la quarantaine de sept jours du projet, sur le même précédent « correctif de CVE signé et urgent » que les entrées fast-uri et js-yaml — attendre la fin de la quarantaine aurait signifié servir sciemment le sanitizer vulnérable trois jours de plus. La liste d'exclusions du garde-fou a été mise à jour dans la même édition et pourra être retirée après le 10/08/2026 ; le forçage reste, c'est le plancher de version
Nouvelle politique de chaîne d'approvisionnement 100 % pnpm. L'application racine et la démo Remotion forment désormais un seul espace de travail pnpm avec un lockfile unique, un gestionnaire pnpm 11.20.0 épinglé par intégrité et une base Node 24.18.1 LTS partagée par les fichiers de version, les engines, la CI et Docker ; npm, npx, pnpx, pnpm dlx, Bun, Yarn et les lockfiles alternatifs sont interdits. Les scripts de cycle de vie des dépendances sont désactivés par défaut avec une liste allowBuilds revue, et la résolution applique une quarantaine de sept jours, une recherche stricte par date de publication, le blocage des sources transitives exotiques, une revalidation d'intégrité auprès du registre et une politique de confiance sans rétrogradation. Des forçages par version majeure résolvent ws en 8.21.0, fast-uri en 3.1.5 et js-yaml en 3.15.1 / 4.3.1. Codex, Claude Code et Gemini partagent un hook de commande fail-closed ; la CI utilise des actions épinglées par SHA et exécute installation figée, vérification des signatures et audit des vulnérabilités modérées et supérieures avant les contrôles de code
Un audit externe de la surface d'attaque a révélé que /[locale]/callback tournait sans authentification, sans wrapper et sans limitation de débit — alors que audit:api-patterns annonçait « coverage OK » depuis le début, parce qu'un route handler Next.js est défini par le nom de fichier route.ts et non par sa présence sous /api, et que le scanner ne parcourait que app/api. Sans limitation, il acceptait un nombre illimité d'appels verifyOtp et exchangeCodeForSession, soit le moyen le moins coûteux d'épuiser le budget Supabase Auth du projet et de bloquer les vraies connexions. La racine de scan est désormais app/ (couverture de 84 à 87 fichiers de route) et le callback est limité au palier standard. Une barrière qui ne voit pas une partie de la surface qu'elle prétend couvrir transforme un oubli en coche verte
Le magic-link n'avait aucun budget par adresse e-mail : la limitation était par IP, or la cible de cet abus n'est pas le serveur mais la boîte de réception d'autrui, et faire tourner les adresses sources contre une même victime autorisait un nombre illimité d'envois — sachant que generateLink crée aussi la ligne auth.users d'une adresse qui ne s'est jamais inscrite, si bien que la même boucle enregistre en masse des adresses arbitraires et brûle la réputation du domaine d'envoi. Désormais 3 envois par tranche de 15 minutes, indexés sur l'adresse normalisée, décomptés après la barrière prelaunch et avant generateLink. Par ailleurs, la fusion des comptes OAuth en doublon reposait sur la seule égalité des e-mails et réécrivait le profil d'un utilisateur existant à partir de données fournies par quiconque terminait le parcours ; elle exige maintenant email_verified dans les métadonnées ou la revendication d'identité
Un nonce CSP mis en cache est un nonce CSP publié. Avec CDN_PUBLIC_CACHE_ENABLED, l'edge stockait le HTML et son en-tête Content-Security-Policy ensemble, donc chaque visiteur pendant la durée de vie du cache recevait le même nonce et pouvait le lire dans le balisage — à ce stade, script-src 'nonce-…' 'strict-dynamic' accorde exactement ce qu'accorde 'unsafe-inline', et précisément sur les pages CMS et blog où une injection compterait. Le middleware retient désormais l'en-tête Cache-Control cacheable sur une réponse porteuse de nonce, sauf si CDN_PUBLIC_CACHE_ALLOW_SHARED_NONCE=true assume explicitement le compromis, ce qui rend la mise en cache HTML à l'edge de nouveau opt-in au lieu d'être active en silence
Le garde-fou SSRF comparait l'IPv6 sous forme de texte et ne reconnaissait donc que les écritures pour lesquelles il avait été rédigé : ::ffff:7f00:1 est 127.0.0.1 et la regex de forme pointée ne le reconnaissait pas, si bien qu'un webhook configuré par un administrateur pouvait viser directement l'interface de loopback ou le point de terminaison de métadonnées du cloud. Même chose pour les préfixes de transition NAT64 (64:ff9b::/96) et 6to4 (2002::/16), et le multicast ff00::/8 n'était pas vérifié du tout. Les adresses sont désormais analysées en hextets numériques et tout IPv4 embarqué est extrait puis jugé comme de l'IPv4 — les nombres ramènent toutes les écritures d'une adresse à une seule valeur, ce que la comparaison textuelle ne peut pas faire. Couvert par 44 nouveaux cas de test
Le limiteur de débit s'exécute désormais avant l'ingestion du corps de requête sur les routes publiques — un attaquant déjà limité faisait auparavant lire au serveur jusqu'à 10 Mo par requête et ne payait le 429 qu'ensuite. La limitation en mémoire ne bascule plus en fail-open en production : sans magasin partagé, chaque instance serverless conserve sa propre table de compteurs et l'attaquant est réparti sur toutes, ce qui n'affaiblit pas les limites mais les supprime, invisiblement ; l'absence d'identifiants Upstash lève désormais une erreur au démarrage, avec ALLOW_INMEMORY_RATE_LIMIT=true comme dérogation explicite pour un déploiement mono-instance assumé. Également : cdnjs.cloudflare.com retiré de script-src (il reste dans img-src), ajout de frame-ancestors 'none' et des en-têtes COOP/CORP sur /api/*, et vérification Turnstile dotée d'un délai d'expiration de 5 s et du transfert de l'IP client
/api/health payait un COUNT(*) complet à chaque requête anonyme : la sonde utilisait { head: true, count: 'exact' } sur profiles, et .limit(1) ne borne pas un comptage — sans authentification ni limitation, un curl équivalait à un parcours de table. L'effet aggravant est qu'il s'agit du point de terminaison interrogé par l'orchestrateur : le saturer faisait répondre degraded/503 et provoquait le redémarrage de conteneurs pourtant sains, une boucle de panne déclenchable depuis l'extérieur par n'importe qui. La sonde sélectionne maintenant une ligne sans comptage, au coût d'une recherche d'index quelle que soit la taille de la table, et porte un limiteur relaxed dimensionné pour ne jamais brider les vraies sondes
Le portail de facturation Stripe était accessible à tout membre d'un espace de travail. createPortalSession n'autorisait que par RLS, ce qui bloque un accountId étranger mais pas un simple membre d'un espace auquel il appartient — or le portail permet d'annuler l'abonnement et de changer le moyen de paiement. Son accountId provient du cookie current_account_id, modifiable côté client, et une Server Action est un point de terminaison HTTP : l'interface conditionnelle qui l'entourait n'a jamais été un contrôle. Elle applique désormais la même barrière getBillingManagerRoles() que la route API a toujours eue. Changement de comportement : en mode b2b, un administrateur d'espace de travail ne peut plus ouvrir le portail depuis /my-account. Les sept écritures du cookie current_account_id partagent maintenant une seule source de nom et d'options ; l'une d'elles perdait les attributs secure et sameSite
Les invitations d'espace de travail n'ont jamais pu être acceptées. invitations.token est généré sur 64 caractères hexadécimaux alors que POST /api/invitations/accept le validait par z.string().uuid() : chaque acceptation était donc rejetée par Zod avant même d'atteindre la couche métier. Le défaut se cachait remarquablement bien : l'e-mail partait, la page d'acceptation affichait correctement le nom de l'espace et celui de l'invitant, et seul le bouton final échouait — en renvoyant « Invalid input », ce qui se lit comme une faute de saisie plutôt que comme un serveur incapable d'analyser son propre jeton. config/workspace.ts détient désormais tokenBytes et un motif dérivé que lisent à la fois le générateur et le schéma, répercuté dans scripts/init-project.js dans la même modification puisque le script d'init réécrit ce fichier
Deux autres façons de rendre une invitation définitivement inacceptable. inviteMember stockait l'adresse saisie telle quelle, si bien qu'une espace finale issue d'un copier-coller ne pouvait jamais correspondre à l'adresse réelle de l'invité — produisant « Cette invitation a été envoyée à une autre adresse e-mail » sans aucun moyen de comprendre pourquoi. Les adresses sont maintenant normalisées via sanitizeEmail() à l'écriture comme à la lecture, si bien que les invitations déjà stockées avec une casse ou des espaces parasites se remettent à fonctionner sans nouvel envoi. La comparaison elle-même avait une faille : undefined !== undefined vaut false, c'est-à-dire « correspond », c'est-à-dire acceptée. Une adresse absente est désormais une non-correspondance, jamais une correspondance
Tout invité suivant un lien alors qu'il était déconnecté atterrissait sur le tableau de bord plutôt que sur l'invitation : la page construisait un redirectTo préfixé par la locale alors que sanitizeRedirectTo attend des chemins sans locale, et /invite/accept n'était de toute façon pas dans la liste d'autorisation. Deux manquements indépendants, un seul effet silencieux — la liste d'autorisation réécrit un échec en /private-dashboard au lieu de refuser, et cette destination est assez plausible pour que personne n'y ait vu un bug. Comme un nouvel invité est déconnecté par définition, cela touchait le parcours normal et non un cas limite. Accepter avec le mauvais compte est aussi devenu diagnosticable : l'écart est détecté côté serveur avant l'affichage du bouton d'acceptation, en nommant l'adresse connectée en entier et l'adresse invitée masquée (les liens d'invitation se transfèrent, la page ne doit donc pas afficher l'adresse d'un tiers), avec une action de déconnexion et bascule de compte
Un tour d'IA en échec affichait un simple 0 à la place de la réponse. /api/ai/stream valide le statut 200 dès le début du flux, si bien qu'une défaillance du fournisseur ne peut être signalée qu'en ligne dans le flux — mais le client déstructurait { content, tokens } de chaque trame et jetait tout le reste, de sorte que l'explication du serveur était perdue et que le tour se terminait vide ; l'interface disposait déjà d'une branche d'erreur avec bouton de relance, jamais atteinte. L'artefact visible venait du second défaut : le tour se terminait avec tokensUsed: 0 et le pied de page des jetons était conditionné par {message.tokensUsed && …}, or React affiche 0 comme nœud de texte là où false et null n'affichent rien. Les erreurs remontent désormais au moment où elles arrivent, localisées via une nouvelle option mapStreamError, et un flux qui se termine sans terminateur ni trame d'erreur ne laisse plus la bulle en saisie indéfiniment
« Gérer l'organisation » apparaissait pour tous les utilisateurs. En mode B2B, chaque utilisateur se voit provisionner un espace de travail à l'inscription et en devient propriétaire : un simple membre de l'organisation d'autrui possède donc aussi une coquille vide bien à lui, et filtrer le menu sur le seul rôle affichait l'entrée à littéralement tout le monde, ce qui transmet la même information que ne l'afficher à personne. La déduction existait à cinq endroits avec trois règles différentes, si bien qu'un administrateur d'espace voyait l'entrée sur /private-dashboard mais pas sur /admin-dashboard. core/accounts/owned-workspaces.ts est désormais la règle unique : propriétaire ou administrateur d'un compte espace de travail porteur d'un abonnement ou comptant plus d'un membre. Deux appelants conservent délibérément leur propre règle et sont commentés pour ne pas être « unifiés » plus tard
Chaque entrée du menu de compte pointait vers la même URL /org-dashboard/admin tandis que le contexte d'organisation résolvait currentWorkspace = ownedWorkspaces[0] : un utilisateur gérant deux organisations ne pouvait tout simplement pas atteindre la seconde. Le menu enregistre maintenant le choix dans le cookie current_account_id existant et le contexte d'organisation le respecte — à titre d'indication seulement : l'identifiant doit correspondre à une entrée de la liste que le serveur a dérivée des adhésions de l'appelant, avec repli sinon, de sorte qu'une manipulation ne peut jamais sélectionner qu'un espace que l'utilisateur gère déjà
Un build Docker tué silencieusement ressemblait à un échec TypeScript — le journal s'arrêtait net après « Running TypeScript ... », sans erreur de type ni ligne d'échec BuildKit, ce qui signifie ensemble un SIGKILL : aucun code de sortie à signaler et aucune chance d'afficher quoi que ce soit. L'étape builder plafonne désormais le tas via un argument de build NODE_BUILD_MEMORY_MB (4096 par défaut), câblé jusqu'à docker-compose.yml pour être surchargeable depuis Coolify sans toucher au Dockerfile. Node dimensionne son old-space par défaut d'après la RAM totale de l'hôte et ne lit pas la limite cgroup du conteneur : le plafonner force V8 à collecter au lieu de croître jusqu'à l'intervention du noyau, et convertit un budget réellement insuffisant en une pile « heap out of memory » lisible plutôt qu'en silence. C'est une assurance, pas un diagnostic confirmé — la principale hypothèse restante est une coupure du canal SSH, puisque Coolify rapporte le code de sortie 255
Deux barrières de CI échouaient pour des raisons sans rapport avec le code. Le garde-fou de démarrage du limiteur de débit se déclenchait pendant next build, qui évalue chaque module de route pour collecter les données de page dans un processus où NODE_ENV vaut déjà production alors que rien n'écoute, et où un runner de build ne détient légitimement aucun identifiant Upstash ; le garde-fou lit désormais aussi NEXT_PHASE, que Next fixe à phase-production-build pendant le build uniquement, si bien que next start et les runtimes serverless le laissent armé et qu'un vrai serveur échoue toujours au démarrage à froid. Et audit:guardrails plantait au lieu de rapporter, parce qu'il lisait sans condition le manifeste d'un espace de travail imbriqué optionnel et mourait sur un ENOENT non capturé avant le moindre contrôle — la liste de manifestes porte maintenant un indicateur required, et l'absence du manifeste racine est rapportée plutôt que levée
Compatibilité de l'installation Docker sous Node 24 : @lingual/i18n-check passé en 0.9.5, qui prend en charge Node.js 20 et supérieur et ne tire plus i18next-parser réservé à Node 22, ce qui corrige ERR_PNPM_UNSUPPORTED_ENGINE lors des installations figées sous Docker. highlight.js, serwist et undici sont déclarés en dépendances racine exactes pour que l'isolation stricte des modules de pnpm expose les paquets directement importés par le rendu Markdown, le service worker et le moteur de jobs pendant les builds Turbopack. Serwist utilise maintenant le mode configurateur au lieu de son plugin Webpack : Next.js compile avec Turbopack par défaut, puis @serwist/cli assemble et injecte le service worker
L'effacement RGPD échouait pour tout utilisateur OAuth disposant d'une photo de profil. process-account-deletions supprimait sans condition dans un bucket Storage avatars dès que profiles.avatar_url était renseigné — or aucun bucket avatars n'existe, ni dans le schéma, ni dans le script d'init, ni dans une migration. Comme avatar_url est alimenté exclusivement depuis les métadonnées OAuth, il s'agit toujours d'une URL externe de fournisseur : Storage répondait « Bucket not found », le processeur levait une exception et la demande était persistée en failed avec retry_count incrémenté — l'effacement au titre de l'article 17 n'aboutissait jamais et la ligne restait bloquée après max_deletion_retries. Les utilisateurs uniquement en magic-link n'étaient pas concernés. Le chemin est désormais nul sauf si l'URL pointe réellement dans le bucket, et le processeur enregistre skipped avec le motif
Le shell des tableaux de bord a été refondu pour exprimer la séparation par l'élévation et la surface plutôt que par des bordures, avec le même habillage sur le tableau de bord privé et celui d'organisation. Le changement qui rend cela possible est un token, pas une passe de feuille de style : --background valait 0.995 face à --card à 1.0, soit un rapport de contraste de 1.020, si bien que seule la bordure séparait une carte de la page ; il vaut désormais 0.94 en clair / 0.115 en sombre, et 0.94 est un plancher et non une préférence — text-destructive sur le libellé de crédits bas ne passe qu'à 4.52 à cette valeur et échoue à 0.93. <main> est devenu un panneau flottant, pleine largeur sur mobile, et le shell est en fixed inset-0 plutôt qu'une boîte h-dvh dans le flux, laquelle dessinait une seconde barre de défilement à côté de celle du panneau. La barre d'onglets basse a été retirée et ses composants supprimés avec elle plutôt que laissés non câblés
Une seule barre d'application porte désormais le déclencheur du tiroir, les notifications, le sélecteur de thème et le menu de compte sur les deux tableaux de bord. Le tableau de bord privé ne pouvait pas en avoir auparavant, pour une raison structurelle : l'état d'ouverture mobile vivait dans la barre latérale, ce qui forçait le déclencheur dans le composant même qu'il ouvrait ; remonter cet état dans un shell constitue tout le changement. Deux conséquences en découlent — la cloche de notifications avait deux montages pour un seul contrôle et ne s'affiche plus qu'une fois, à toutes les largeurs, et le menu de compte a rejoint la barre, ce qui referme une divergence où le tableau de bord privé affichait son propre menu artisanal, avec un chemin de déconnexion distinct et son propre client Supabase, alors que le site vitrine, l'admin et l'organisation en partageaient un seul
Migration du design system sur ~84 pages. La couleur tient désormais à un seul paramètre : --brand-h, --brand-c, --brand-c-subtle et --neutral-c sont les seules valeurs de couleur écrites à la main, et tous les tokens de marque, de neutres, de barre latérale et de graphiques en dérivent, si bien qu'une seule ligne suffit à rebrander le produit — l'usage de la palette brute est passé de 53 fichiers à 2, deux exceptions documentées. Les états sémantiques ne dérivent délibérément pas de la marque et se situent tous à une luminosité de 0.55, parce qu'un token utilisé comme texte a une exigence de contraste strictement plus dure que le même token utilisé comme fond. Nouvelles échelles de conteneurs, d'élévation, de mouvement et de z-index, échelle typographique fluide en clamp(), et correction des variables de police qui pointaient vers des valeurs --font-geist-* définies nulle part, si bien que font-sans retombait sur la police par défaut du navigateur. Nouvelle bibliothèque de patterns (PageHeader, StatCard, EmptyState, ErrorState), avec 60 appels à PageHeader et plus aucun titre de page artisanal dans les tableaux de bord
La navigation est pilotée par manifeste : config/navigation.ts contient les manifestes privé, organisation et admin ainsi que des fonctions pures de résolution et de correspondance d'état actif, en remplacement de 2 049 lignes de barres latérales quasi dupliquées, les icônes étant portées par leur nom et non comme composants, puisque les conditions se résolvent côté serveur et que React ne peut pas sérialiser une fonction à travers cette frontière. 31 tests figent la sémantique des conditions et des libellés. components/navigation/sidebar-nav-link.tsx est maintenant l'unique implémentation de lien derrière les trois barres latérales — les copies privée et organisation avaient toutes deux acquis un aria-label pour le rail replié, jamais la copie admin, de sorte que toute la navigation repliée de l'admin était anonyme pour les technologies d'assistance. Motion est installé et monté uniquement dans le layout vitrine (LazyMotion + MotionConfig reducedMotion="user"), ~4,6 ko de base et zéro octet sur les tableaux de bord
Nouvelle route /register qui partage les mécaniques de magic-link et de code à usage unique avec /login via une prop variant ne sélectionnant que les textes et le cadrage, délibérément absente de la liste d'autorisation de redirection puisqu'elle est l'origine d'un parcours d'authentification et jamais une destination post-authentification. input-otp remplace le champ de code artisanal sur les deux pages et apporte la répartition du collage, le retour arrière vers la case précédente, la navigation aux flèches et l'autocomplétion one-time-code. La primitive chart de shadcn remplace les barres d'analytics de l'admin, qui étaient des div positionnées à la main avec un plancher de hauteur à 2 % pour simuler la visibilité, sans axe Y, avec des libellés en X désalignés et une infobulle au survol inatteignable au clavier ou au toucher ; Recharts se résout en exactement un chunk de 343 ko, absent du jeu initial de chaque page
Nouveau pattern StatusPill sur 13 emplacements de rendu, dont huit ne transmettaient le statut que par la couleur et le texte, avec « annulé » et « échoué » partageant un glyphe et séparés par la seule couleur. Le constat qui l'accompagne révise le style maison : la forme teintée que le projet documentait comme canonique échoue pour le petit texte — bg-success/10 avec text-success mesure 4,19:1 et info 4,40:1 face à une exigence de 4,5:1, parce que teinter une surface avec la teinte même du token rapproche la surface du texte et consomme exactement la marge pour laquelle le token avait été calibré. StatusPill garde donc la teinte sur l'icône, où le seuil de 3:1 s'applique, et rend le libellé en text-foreground
Environ 50 libellés d'accessibilité codés en dur en anglais ont été remplacés. aria-label, sr-only, alt et title ne sont jamais affichés pour les personnes voyantes, et axe vérifie qu'un libellé existe et non dans quelle langue il est rédigé : toutes les barrières restaient donc vertes pendant qu'un utilisateur francophone de lecteur d'écran entendait « Toggle theme », « Send message », « Close menu ». L'éditeur WYSIWYG concentrait à lui seul 21 libellés de barre d'outils derrière une prop translations optionnelle dont l'unique appelant ne passait rien. Un nouveau UiStringsProvider résout côté serveur un ensemble restreint de chaînes d'habillage et les transmet par contexte aux primitives client profondes, DialogContent comptant 41 appels et le passage par props n'étant pas envisageable ; il lève une erreur s'il n'est pas monté plutôt que de retomber sur l'anglais. Une nouvelle barrière permanente analyse les composants et les routes localisées à la recherche de littéraux dans les quatre attributs, et a été prouvée capable d'échouer avant d'être jugée fiable. Également corrigés : des contrôles de barre latérale à icône seule sans aucun nom accessible, /login et /register dépourvus de <h1>, et le layout d'authentification dépourvu de <main> pour que le lien d'évitement mène quelque part
La notice publique de transparence au titre du règlement européen sur l'IA affichait des clés i18n brutes dans toutes les langues depuis sa mise en ligne — son <h1> affichait legal.aiTransparency.title et chaque <h2> était un chemin pointé. Toutes les barrières étaient vertes, et la raison mérite d'être retenue : le test de parité compare les catalogues entre eux, donc une clé uniformément absente de tous est parfaitement cohérente ; axe accepte un chemin pointé comme titre valide ; tsc l'accepte parce que le traducteur renvoie la clé lorsqu'il ne la résout pas ; et aucun test ne visitait la route. 40 clés de véritable contenu de conformité français et anglais décrivent désormais les mécanismes réellement implémentés dans le code, vérifiées à l'exécution sans aucune clé brute résiduelle
Conformité à l'article 50 du règlement européen sur l'IA. L'article 50 est applicable depuis le 2 août 2026 et a été explicitement exclu du report du Digital Omnibus, or le boilerplate livrait un assistant conversationnel sans information sur l'interaction avec une IA ni marquage des sorties. Le nouveau config/ai-act.ts est la source unique de vérité pour le niveau de risque, les onze catégories de l'annexe III et les informations par fournisseur, toute déclaration d'un usage relevant de l'annexe III faisant remonter le niveau quelle que soit la configuration. L'information au titre de l'article 50(1) s'affiche à deux emplacements par surface de conversation (état vide et zone de saisie, pour survivre au défilement) ; le marquage au titre de l'article 50(2) est écrit durablement dans chat_messages.ai_generated et ai_provider sans condition — un message est généré par IA que l'exploitant expose ou non le marquage — auxquels s'ajoutent des en-têtes de réponse X-AI-* ne portant que le modèle et le fournisseur. Nouvelle notice publique /ai-transparency, nouvelle section des CGU sur les fonctionnalités d'IA, et cinq documents pour l'exploitant : guide du règlement IA, registre des traitements (article 30), trame d'AIPD (article 35), procédure de violation de données et registre des sous-traitants et clauses DPA
Lacunes RGPD comblées en parallèle du chantier règlement IA. L'export au titre de l'article 15 ne couvrait pas du tout les programmes de parrainage et d'affiliation — codes de parrainage, parrainages, candidatures d'affiliation, affiliés, attributions et conversions contiennent tous des données personnelles de la personne concernée et rien n'était exportable ; sept sections ajoutées, 28 au total, avec une passe côté affilié distincte parce que deux de ces tables sont indexées sur affiliate_id et non sur account_id. Le consentement n'était pas davantage démontrable au sens de l'article 7(1) : user_consents est mis à jour par upsert, donc chaque changement écrasait la décision précédente et seul l'état courant était prouvable. Une nouvelle table user_consent_events en ajout seul porte désormais la traçabilité, avec SELECT limité à ses propres lignes et aucune politique INSERT/UPDATE/DELETE (un journal d'audit dans lequel la personne concernée peut écrire n'est pas une preuve), initialisée depuis l'état courant, et les deux tables ont gagné policy_version — un consentement donné face à une notice remplacée n'est pas un consentement à la notice actuelle
Les e-mails d'alerte administrateur livraient des clés i18n brutes — les notifications d'échec de job et les alertes de crédits LLM arrivaient sous la forme billingNotif.undefined.subject avec un corps composé de chemins pointés, l'alerte réelle étant perdue avant l'envoi. sendAdminNotification() mettait en file une charge utile pré-rendue sous email_type = 'notification', or ce type signifie « charge utile de facturation portant un discriminant kind » pour le worker, qui la passait au gabarit de facturation ; le traducteur renvoie la clé telle quelle lorsqu'il ne la résout pas, et les clés ont donc été expédiées comme contenu. Nouveau email_type = 'admin' avec une branche de worker dédiée qui transmet le corps pré-rendu intact, une migration qui reclasse les lignes déjà en file, et une garde à l'exécution qui fait échouer bruyamment une charge utile incohérente au lieu de livrer discrètement du texte de remplacement. Une seule forme de charge utile par email_type est désormais un invariant énoncé. Les alertes émises pendant la période de bug sont récupérables depuis pending_emails.payload
La publication par langue du CMS fuyait d'une langue à l'autre : le contrôle de publication reposait sur la chaîne de repli d'affichage, si bien qu'une ligne sans clé en-US héritait de fr-FR: true — la page passait en ligne en anglais avec du contenu français, était émise dans le sitemap et annoncée comme alternative hreflang anglaise. La publication et les directives SEO (canonical, noindex, nofollow) se résolvent désormais via des accesseurs qui s'arrêtent aux variantes de même langue et ne retombent jamais sur la langue par défaut, tandis que les champs d'affichage conservent la chaîne permissive. hreflang n'annonce plus que les traductions réellement en ligne, le sitemap émet hreflang et ne contredit plus les pages qu'il liste, et il se revalide toutes les heures au lieu d'être figé au build — une lecture en base n'est pas une API dynamique pour Next, si bien que les pages publiées après un déploiement n'apparaissaient jamais. Les slugs réservés sont unifiés en une seule liste, et le sélecteur de langue affiche désormais les langues indisponibles en atténué et y redirige vers l'accueil ou l'index du blog de cette langue plutôt que vers une 404
Next.js 16.2.9 → 16.3.0, avec l'analyseur de bundle et la configuration ESLint alignés, ce qui referme cinq avis de sécurité : SSRF dans les rewrites via un nom d'hôte de destination contrôlé par l'attaquant, divulgation non authentifiée de points de terminaison internes de Server Functions, confusion de cache sur les corps de réponse en cas de requête UTF-8 invalide, charge utile de Server Action non bornée dans le runtime Edge, et déni de service de l'optimisation d'images via SVG. npm audit est passé de 12 vulnérabilités à 0 sur l'arbre complet, en --omit=dev et via la barrière d'audit, avec sharp, undici, postcss et d'autres corrigés dans les plages autorisées. Un forçage ciblé maintient @lhci/cli à jour au lieu d'accepter le correctif --force suggéré par npm, qui le rétrograderait de 0.15.1 à 0.6.1 ; isomorphic-dompurify est délibérément maintenu en 2.x et documenté comme épinglé volontairement
Couverture d'accessibilité et de responsive étendue de 5 à 12 routes (30 → 68 tests), avec analyse de /register, /blog, /privacy, /legal, /changelog, /ai-transparency et /privacy-choices en thème clair et sombre et aux largeurs 320/768/1280. Le « 30/30 vert » annoncé pendant dix phases de travaux sur le design system n'avait jamais mesuré que cinq routes vitrine, ce qui explique que le défaut de clés du règlement IA, un échec de contraste et toute une classe de contrôles de barre latérale sans nom soient passés au travers. Trois défauts révélés et corrigés : un lien d'évitement pointant vers une cible inexistante sur toutes les routes d'authentification, un € codé en dur dans le formateur de coûts des analytics affiché aux administrateurs en-US à côté de montants en $, et un décalage UTC d'un jour qui étiquetait les barres des graphiques avec la veille dans les fuseaux à décalage négatif. Une couverture Playwright authentifiée optionnelle a également été ajoutée — le fixture génère un token_hash via l'API d'administration et pilote le navigateur vers le callback de l'application pour que le vrai verifyOtp s'exécute, et reste inerte tant que E2E_TEST_EMAIL n'est pas défini
vitest exécutait la suite de tests d'une dépendance vendorisée : la liste d'exclusion utilisait 'node_modules' seul, ce qui en tant que glob ne correspond qu'au répertoire de premier niveau, si bien que le runner descendait dans le node_modules d'un espace de travail imbriqué et exécutait les tests de la bibliothèque zod comme s'ils étaient ceux du projet. Les 173 fichiers / 2 006 tests annoncés étaient en réalité 14 fichiers / 260 tests, et les trois « fichiers de test en échec préexistants » balayés dans plusieurs entrées passées appartenaient à zod, en échec sur une dépendance pair non satisfaite. Aucun test du projet n'a été perdu ; le chiffre mesure simplement le projet désormais. Des composants morts ont été supprimés dans la même passe — tout le répertoire components/dashboard/ et une copie inutilisée de l'interface de chat, tous vérifiés à zéro importateur, formes d'import dynamique comprises
Toutes les compétences de design IA vendorisées ont été réalignées sur leur source amont. Impeccable v4.0.4 a replié une vingtaine de compétences autonomes en sous-commandes (45 compétences → 26) : /typeset, /shape, /critique et /polish ne se résolvent donc plus seuls, /arrange est devenu /impeccable layout, et /normalize est retiré sans successeur. La v4 embarque aussi ~40 scripts exécutables et déclare des outils Bash autorisés, dont un écrivain de configuration de hooks : la règle design-skills documente désormais cette surface d'exécution, avec la consigne permanente de la réexaminer à chaque montée de version. Ajout de la compétence officielle /shadcn à côté de tailwind-v4-shadcn (elles ne se recouvrent pas — l'une couvre les composants, la CLI et le registre, l'autre la configuration Tailwind v4 et ses pièges), mise à jour de ui-ux-pro-max en v2.11.0 (84 styles, 192 palettes, 74 appariements de polices, 22 stacks), et ajout de Saaspo comme référence partagée de recherche design SaaS. emil-design-eng n'a délibérément pas été mis à jour : tout son delta amont retire les recommandations Radix au profit de Base UI, or cette stack est shadcn/ui sur Radix
v1.32Minor
Retour visuel à la connexion OAuth, corrections du checkout des licences, pin de Next.js et remédiation de la revue de code v2
Ajoute un retour de chargement en ligne à la connexion OAuth, corrige le flux de checkout des licences de bout en bout (user_id atteint désormais le webhook Stripe, les achats ne sont donc plus silencieusement perdus), rend la carte de facturation de mon compte sensible au modèle de facturation, fige les paquets de la release-train Next.js, et finalise toute la remédiation de la revue de code v2 sur la sécurité, l'architecture et la performance.
Les boutons de connexion OAuth de la page de login affichent désormais un spinner en ligne et un libellé localisé « Connexion à {provider}… », et désactivent tous les fournisseurs pendant qu'une redirection est en cours (components/auth/oauth-buttons.tsx). signInWithOAuth fait un aller-retour réseau pour résoudre l'URL d'autorisation du fournisseur avant que le navigateur ne navigue, si bien que les boutons ne donnaient auparavant aucun retour pendant ce délai et paraissaient inertes. Ajout de aria-busy sur le bouton actif et d'une garde contre les double-clics / le lancement de deux fournisseurs en parallèle ; en cas d'erreur l'état se réinitialise pour permettre une nouvelle tentative, en cas de succès le spinner reste affiché puisque la page navigue déjà
Les sessions de checkout de licence portent désormais user_id dans leurs métadonnées Stripe (createLicenseCheckoutSession dans core/billing/checkout.ts). Le handler checkout.session.completed se protège contre l'absence de account_id/user_id, donc chaque achat de licence était silencieusement abandonné au webhook (« Checkout session missing required metadata ») — la licence n'était jamais créée et les crédits jamais accordés. Cela concernait à la fois le LicensePricingTable du tableau de bord et le flux de la landing. user_id alimente désormais aussi licenses.purchased_by et la ligne d'audit payments (toutes deux auparavant nulles)
Les achats de licence depuis la landing aboutissent désormais de bout en bout. checkout-client.tsx a gagné une branche type: 'license' qui POST vers /api/billing/license-checkout (auparavant les licences passaient par la garde !priceId et levaient « Payment configuration not available for this plan » — les licences portent un productId, pas un priceId Stripe)
landing-licenses.tsx pose désormais le cookie httpOnly bsk_pending_checkout (parité avec le flux d'abonnement, pour que les connexions par magic-link inter-onglets conservent la sélection) et utilise un redirectTo=/checkout sans locale et en liste blanche. L'ancien redirectTo préfixé par la locale et porteur de query était rejeté par sanitizeRedirectTo et renvoyait les acheteurs déconnectés vers /private-dashboard avant qu'ils n'atteignent le checkout
La carte de facturation de /my-account est désormais sensible au modèle de facturation : en mode license elle affiche les libellés « Licence » / « Gérez votre licence et votre accès » / « Aucune licence active » au lieu des chaînes centrées abonnement, et le badge d'accès à la licence affiche le nom de produit localisé (depuis pricingConfig.licenses) au lieu de l'identifiant brut du produit. Les modes subscription et hybrid conservent le libellé d'abonnement
Pin des paquets de la release-train Next.js à ^16.2.9 (depuis ^16.2.6) : next, eslint-config-next et @next/bundle-analyzer. next se résolvait déjà à 16.2.9 (dernière 16.2.x), donc cela ne fait que relever le plancher déclaré pour qu'une installation neuve ne puisse pas retomber sur 16.2.6–16.2.8 ; @next/bundle-analyzer était la seule vraie mise à niveau. Les trois bumpés de concert pour éviter un décalage config-ESLint / analyzer avec le framework. Ne lève pas l'avis postcss (GHSA-qx2v-qp2m-jg93), uniquement au build, que seule une future version de Next pourra résoudre
CRITIQUE — Correction de la politique INSERT obsolète sur admin_logs dans le bloc miroir des migrations en bas de supabase/schema.sql (ARCH-1) : le bloc recréait la politique is_admin WITH CHECK d'avant SEC-M4, donc tout supabase db reset neuf (dev, CI, forks en aval) rouvrait silencieusement la faille de falsification du journal d'audit corrigée par 20260610090002. Le miroir recrée désormais la politique réservée au service_role. Les environnements migrés de façon incrémentale n'ont jamais été affectés
La fusion de comptes OAuth est désormais atomique (SEC-4) : un nouveau RPC SECURITY DEFINER merge_oauth_duplicate_user (EXECUTE réservé au service_role + garde auth.role()) supprime le compte personnel/profil de l'utilisateur en double et reporte les données d'affichage OAuth en une seule transaction. auth.admin.deleteUser ne s'exécute qu'après le commit ; en cas d'échec, le flux se termine quand même en « compte lié » (ligne auth orpheline journalisée) au lieu de recréer des lignes pour l'utilisateur fusionné
Le sameSite du cookie CSRF est passé de 'strict' à 'lax' aux trois points de définition (SEC-3) — 'strict' supprimait le cookie sur les navigations top-level cross-site (liens d'e-mail), désynchronisant la paire double-submit en 403 parasites. Le token n'est pas secret ; la validation Origin/Referer et la comparaison double-submit sont inchangées. Ajout d'un test d'invariants d'architecture (SEC-5) fixant les contraintes porteuses UNIQUE(referred_account_id) et le CHECK d'auto-parrainage pour qu'un refactor ne puisse pas supprimer silencieusement la garantie anti-farming
Consolidation du domaine des licences (ARCH-2/3) : la lecture par lot des licences du contrôle d'accès unifié et la liste admin des licences passent désormais par de nouveaux lecteurs core/licenses/queries.ts ; suppression de 9 fonctions de licence mortes plus des types inutilisés. Suppression du cluster d'exports core/ non câblés sur accounts/affiliates/roles/chat (ARCH-4) ; les lecteurs de chat vivants ont perdu leur suffixe Uncached trompeur. Contrôles de rôle consolidés sur hasRole() et bascule de redirection post-auth unifiée derrière getPostEventRedirect() (ARCH-5/6) ; l'expiration des invitations dérive désormais de workspaceConfig et resendInvitation() a migré vers core avec la validation Zod qui lui manquait (ARCH-7/8/11)
Réécriture du job generate-analytics (PERF-1/2) : le résumé quotidien d'usage IA appelle désormais le RPC d'agrégation get_admin_ai_period_stats au lieu de télécharger chaque ligne ai_requests du jour et de réduire en JS, et ses trois lectures indépendantes s'exécutent en Promise.all. check-low-credits-alerts a déplacé son filtre de solde dans la BDD au seuil configuré le plus large (PERF-3) ; cleanup-push-subscriptions exécute ses trois suppressions indépendantes en parallèle (PERF-4)
v1.31Patch
Remédiation de la revue de code complète — sécurité, architecture, performance et base de données
Une large passe de remédiation issue d'une revue de code complète : gardes recent-auth et validation Zod sur les routes admin destructrices, écritures du journal d'audit réservées au service_role, blocklist SSRF étendue et retrait des SVG du bucket public, restauration d'un contrôle RLS admin bicolonne, nettoyage d'index redondants et nouveaux composites de chemin chaud, agrégations d'analytics admin (~100k lignes) déplacées dans des RPC SQL, élimination de la double authentification sur les routes chat/parrainage, et une série de correctifs d'architecture et d'i18n.
Ajout de la validation Zod (union discriminée, licenseId UUID, days borné) et de la garde requireRecentAuth() à la route admin de révocation/extension de licence — révoquer une licence reprend des crédits et un accès payant, donc une session admin détournée et périmée ne peut plus le faire en un seul POST. requireRecentAuth() ajouté aussi aux routes d'annulation de parrainage et de désactivation de code de parrainage, comblant l'écart avec la route structurellement identique d'annulation de conversion d'affilié
INSERT sur admin_logs restreint au service_role (20260610090002) : l'ancien WITH CHECK is_admin permettait à n'importe quel admin de forger des lignes d'audit attribuant des actions à un autre admin via la Data API. Toutes les écritures légitimes passaient déjà par supabaseAdmin ; le SELECT reste lisible par les admins
Arrêt des fuites du texte d'erreur Postgres/RPC brut depuis les quatre routes cron admin — les clients reçoivent désormais une erreur générique avec un code tandis que le message complet reste dans error_logs. L'inscription/désinscription newsletter ne renvoie plus la chaîne d'erreur brute du fournisseur d'e-mail en 500, et le chemin d'échec fournisseur de désinscription est désormais journalisé
Extension de la blocklist SSRF des IP privées (lib/security/ssrf.ts) avec la plage partagée CGNAT 100.64.0.0/10 (RFC 6598), 192.0.0.0/24 (RFC 6890) et 198.18.0.0/15 (RFC 2544) — les URLs de webhook configurées par les admins ne peuvent plus atteindre ces plages
Retrait de image/svg+xml du bucket media public (20260610090006) et de la liste blanche côté application, supprimant avec lui le filtre de contenu SVG par regex contournable. Les SVG peuvent embarquer des scripts et le bucket est public ; utilisez PNG/WebP ou servez des SVG de confiance depuis /public
Correction de user_is_account_admin qui perdait la branche role_slug (20260610090000) : la migration de pin du search_path du 29 mai avait recréé la fonction avec le seul contrôle role hérité, refusant silencieusement la gestion des membres aux admins à rôle personnalisé dont l'adhésion ne renseigne que role_slug. Contrôle bicolonne restauré partout, y compris le CREATE OR REPLACE ultérieur qui l'emporte sur un db reset neuf
Suppression d'index redondants/chevauchants (20260610090005, 20260610090008) pour réduire l'amplification d'écriture, et ajout d'index composites de chemin chaud via des migrations CONCURRENTLY autonomes : api_keys(account_id, created_at DESC), documents(account_id, created_at DESC) et referrals(ip_hash, created_at DESC) soutenant le comptage de rate-limit IP d'apply_referral_code
Remplacement de l'agrégation JS des analytics admin (~100 000 lignes ai_requests par rendu de /admin-dashboard/analytics) par 8 RPC d'agrégation SQL (20260610090001) : get_admin_ai_period_stats, get_admin_active_user_count, get_admin_credit_flow, get_admin_daily_stats, get_admin_top_users, get_admin_model_stats, get_admin_agent_stats, get_admin_org_stats — tous SECURITY DEFINER, EXECUTE réservé au service_role, gardés par auth.role() dans le corps
Suppression du double auth.getUser() + double fetch des memberships sur 8 routes chat-session et parrainage : le nouveau checkPermissionForUser(userId, accountId, permission) réutilise l'utilisateur déjà authentifié par apiSecurity.* — ~60–120 ms gagnées par requête
Bornage des requêtes de liste non bornées (listDocumentsForAccount plafonné à 200 avec metadata retiré de la projection de liste, api_keys plafonné à 100), parallélisation des awaits indépendants du layout du tableau de bord admin, et déplacement d'isomorphic-dompurify (~150 Ko) hors du bundle client du formulaire de page CMS vers un composant de prévisualisation dynamic ssr:false. Plancher de performance Lighthouse CI relevé de 0,7 à 0,85
Remplacement de 7 contrôles d'owner bicolonne en ligne par le helper partagé hasRole() (mon compte, layout admin, pages abonnements/organisations/licences/détail org). Cela a fait surface et corrigé deux bugs latents : la page licences ne vérifiait que role_slug et la page détail-org ne vérifiait que role — les deux requêtes sélectionnent désormais les deux colonnes
Extraction de l'orchestration service_role en ligne des trois pages admin les plus lourdes vers des modules core : users → core/organizations/admin-users.ts, organizations → core/organizations/admin-organizations.ts, subscriptions → core/billing/admin-subscriptions.ts. Sans changement de comportement ; les pages sont désormais de fines coquilles Server Component
Correction du formatage de nombres/dates sans locale sur les KPI + le tableau des conversions du tableau de bord affiliés, ReferralStatsRow et landing-licenses.tsx (désormais via getIntlLocale(locale)). Câblage du producteur createInAppNotification jusqu'alors non branché : le job check-low-credits-alerts crée désormais une notification cloche pour l'owner du compte en plus du push
Mise à jour des règles IA canoniques et de la doc utilisateur pour coller à la nouvelle réalité du code (liste des actions recent-auth, famille de RPC d'analytics, interdiction des SVG, guidance checkPermissionForUser(), cible Lighthouse 70 % → 85 %) et synchronisation des miroirs de règles .codex / .gemini / .opencode
v1.30Patch
Garde owner-mint à l'INSERT sur les adhésions, restriction RGPD des clés API et lookup d'authentification résilient
Ferme une voie d'escalade de privilèges intra-tenant sur memberships (un admin non-owner pouvait créer un owner via un insert direct sur la Data API), restreint la section api_keys de l'export RGPD aux owners/admins, et ajoute un wrapper d'authentification edge-safe pour que les sessions Supabase manquantes ou expirées soient traitées comme anonymes au lieu de lever une exception. Plus un correctif du script d'init pour que npm run init ne supprime plus le bloc de configuration CDN.
Fermeture d'une voie d'escalade de privilèges intra-tenant sur memberships : la garde owner-mint (lock_membership_sensitive_columns) était liée uniquement en BEFORE UPDATE, alors que la politique RLS « Admins can insert memberships » vérifie le statut admin du compte sans contraindre le rôle inséré — un admin non-owner pouvait donc faire POST /rest/v1/memberships avec role='owner' pour un utilisateur complice et l'élever au rang d'owner (pouvoirs de facturation + suppression). Le trigger est désormais BEFORE INSERT OR UPDATE et rejette les inserts/updates owner provenant d'appels Data API
Correctif à chaud du périmètre de rôle de la garde : la première version utilisait auth.role() is distinct from 'service_role', ce qui attrapait le trigger d'inscription SECURITY DEFINER handle_new_user_account() (il s'exécute sur la connexion auth-admin GoTrue où auth.role() vaut NULL) et faisait échouer l'inscription avec « Database error saving new user ». Reciblé sur auth.role() in ('authenticated','anon') pour que l'escalade reste bloquée sur les écritures PostgREST directes tandis que les triggers internes et le bootstrap service_role sont exemptés
Restriction de la section api_keys de l'export RGPD aux comptes où le demandeur est owner/admin (via getOrgManagerRoles() + hasRole()), alignée sur le contrôle de l'UI de gestion (requireApiKeyManager) pour qu'un simple membre ne puisse plus exporter les noms/préfixes/scopes de clés qu'il ne peut pas voir autrement
Ajout de lib/auth/safe-get-user.ts (safeGetUser) — un wrapper edge-safe qui traite une session Supabase manquante/expirée/invalide comme anonyme au lieu de laisser @supabase/ssr lever AuthApiError: Invalid Refresh Token (qui apparaissait en 500 non géré / bruit dans les logs). Branché sur chaque appel serveur à auth.getUser() (get-user.ts, proxy.ts, api-security.ts, permissions/check.ts, verify-admin.ts, stripe/actions.ts, ai/stream). Échoue côté fermé — les routes protégées redirigent toujours, les routes authentifiées renvoient toujours 401
Correction de scripts/init-project.js qui régénérait config/app.ts sans le bloc cdn ni les exports isPublicCacheablePath() / PUBLIC_CACHEABLE_PATHS. La fonctionnalité CDN (1.29) les avait ajoutés à la config commitée, au prompt du wizard et à .env.example, mais le template generateAppConfig() n'avait jamais été mis à jour — donc lancer npm run init écrasait config/app.ts en supprimant 57 lignes, cassant proxy.ts
Report du bloc cdn, de PUBLIC_CACHEABLE_PATHS et de isPublicCacheablePath() dans le template generateAppConfig() pour que les futurs inits émettent une config complète, et ajout de l'anti-pattern A15 : tout champ/helper ajouté à un fichier de config régénéré par init-project.js DOIT être reporté dans le template generate*() correspondant dans le même changement — traité comme une unité d'édition, comme le miroir migration↔schema.sql (A1)
v1.29Minor
Renforcement sécurité, jobs et performance base de données, rendu des slugs dynamiques, cache CDN optionnel et SEO/localisation
Une large passe de durcissement sur la sécurité, les jobs en arrière-plan et la performance de la base de données : politiques d'adhésion et de suppression de compte resserrées, validation SSRF des webhooks centralisée, synchronisation Stripe réservée au service role, RPC d'agrégation pour les compteurs du tableau de bord, et search_path figé sur les fonctions SECURITY DEFINER. Plus un correctif de rendu des slugs dynamiques blog/CMS en production, une couche de cache CDN edge activable et des améliorations SEO/localisation (html lang rendu côté serveur, URLs du site normalisées, meilleure négociation de locale).
Durcissement des mises à jour de memberships avec WITH CHECK plus un trigger qui bloque la réassignation de compte/utilisateur hors service role et l'auto-élévation du propriétaire via role ou role_slug
Refactorisation de /api/admin/jobs/** en routes légères adossées à core/jobs/*, validation SSRF des webhooks centralisée dans lib/security/ssrf.ts, et documentation des vérifications DNS partagées à la création et à l'exécution
Refonte de sync-stripe pour grouper les mises à jour d'état des abonnements existants via un RPC réservé au service role, tout en gardant la création des lignes manquantes dans le flux du domaine facturation
Ajout de l'usage de RPC d'agrégation pour les statistiques IA admin/org et documentation du pattern « pas de téléchargement de lignes brutes » pour les compteurs du tableau de bord
Mémoïsation des wrappers CMS unstable_cache() par clé canonique et documentation du pattern pour les futurs caches à clé dynamique
Resserrement des échecs d'export RGPD : les messages internes par section sont désormais journalisés côté serveur et le client reçoit une réponse générique EXPORT_FAILED
Fermeture d'une voie d'élévation de privilèges intra-tenant sur account_deletion_requests : la politique UPDATE d'annulation par le propriétaire porte désormais un WITH CHECK, et un trigger BEFORE UPDATE de verrouillage de colonnes restreint les propriétaires hors service role à une transition status → 'cancelled', de sorte qu'un membre ou propriétaire ne peut plus modifier cascade_members (ni aucun autre champ) via la Data API pour forcer la suppression d'un compte membre
Fixation de SET search_path = public sur les helpers d'adhésion SECURITY DEFINER (user_belongs_to_account, user_is_account_admin, user_has_permission) et les triggers lock_membership_sensitive_columns / lock_notification_columns
Ajout d'un WITH CHECK correspondant à la politique storage.objects « Admins can update media » pour qu'un admin ne puisse pas déplacer un objet média hors du bucket media
Restriction du CHECK pending_emails.email_type à l'ensemble pris en charge par le worker (contact, newsletter, notification) afin que les types non supportés échouent dès l'insertion plutôt que d'être silencieusement marqués en échec permanent
Consolidation de la validation SSRF des URLs de jobs admin dans core/admin/settings.ts sur le résolveur canonique validatePublicWebhookUrl (suppression d'une copie regex/DNS divergente)
Remplacement du reduce JS du chiffre d'affaires de l'aperçu admin par un RPC d'agrégation get_admin_revenue_sum et encapsulation de getPlatformStats dans unstable_cache (60s, par locale)
Suppression d'index mono-colonne redondants couverts par des composites (credit_tx_account_idx, subscriptions_account_idx) et ajout de composites pour les chemins critiques du chiffre d'affaires (payments(status, created_at desc)) et de l'historique des jobs (job_runs(job_id, started_at desc))
Déplacement de la route de détail documents/[id] vers un dispatch léger sur un nouveau lecteur core/documents, mémoïsation du wrapper de cache blog par post, suppression d'un appel getUser() redondant à l'onboarding (désormais via SecurityContext.user_metadata), parallélisation des boucles par liste du fournisseur d'emails, suppression d'environ 20 casts as any obsolètes sur des tables désormais typées, et regroupement des helpers de notifications in-app dans core/notifications/
Correction d'un 500 DYNAMIC_SERVER_USAGE en production sur les articles de blog / pages CMS publiés après le dernier build : blog/[slug] et [slug] sont désormais force-dynamic (les données restent mises en cache via unstable_cache), de sorte que l'ISR à la demande n'entre plus en conflit avec la lecture headers() du nonce CSP par requête du layout racine
Ajout d'une couche de cache CDN edge activable (CDN_PUBLIC_CACHE_ENABLED, CDN_PUBLIC_S_MAXAGE, CDN_PUBLIC_SWR) : une fois activée, le middleware annonce un Cache-Control cacheable sur les requêtes GET anonymes vers les pages de contenu public uniquement (sans cookie d'auth, sans Set-Cookie), en préservant le nonce CSP strict par requête tandis que les réponses authentifiées/tableau de bord/API restent private, no-store. Désactivé par défaut ; documenté dans caching.md, .env.example et la doc de cache en production
Rendu côté serveur de la locale réelle de la route sur <html lang> (fr-FR, fr-CH, en-US, en-CA) au lieu de corriger une langue par défaut après hydratation, améliorant la précision pour les crawlers et les outils d'accessibilité
Centralisation de la normalisation de l'URL du site pour les URLs canoniques, les alternates hreflang, OpenGraph/JSON-LD, le sitemap et les métadonnées robots, afin que les slashs finaux dans NEXT_PUBLIC_APP_URL ne produisent plus de doubles slashs
Amélioration de la négociation de locale pour les requêtes sans préfixe afin qu'Accept-Language puisse rediriger vers les locales régionales exactes configurées, comme en-CA
v1.28Patch
Synchronisation du widget crédits de la barre latérale privée, grants Data API Supabase explicites et skills d'agent Supabase rafraîchis
Le widget crédits de la barre latérale du tableau de bord privé reste désormais synchronisé avec les props faisant autorité côté serveur après des actions serveur comme Fin d'essai ; une nouvelle migration rend explicites les grants Data API Supabase pour les nouveaux projets et les réinitialisations de base de données, avec la documentation rafraîchie ; les skills locaux de l'agent Supabase et les entrypoints des outils IA sont mis à jour pour invoquer à la fois le skill Supabase produit et le skill des bonnes pratiques Postgres.
Le widget crédits de la barre latérale du tableau de bord privé se met désormais à jour immédiatement après les changements de crédits côté serveur (notamment la confirmation de Fin d'essai) sans nécessiter de rechargement complet de la page
Cause racine corrigée : l'initialiseur paresseux de useState(initialCreditsBalance) ne s'exécutait qu'au montage, donc router.refresh() re-rendait le layout avec une nouvelle prop faisant autorité côté serveur tandis que la barre latérale conservait son état local obsolète — ajout d'un useEffect qui resynchronise creditsBalance chaque fois que la prop initialCreditsBalance change, tout en préservant l'écouteur 'credits-updated' existant pour les décrémentations optimistes de chat
La barre latérale d'organisation lisait déjà workspace.creditsBalance directement depuis les props, aucun changement n'était donc nécessaire de ce côté
Nouvelle migration 20260527225001_explicit_data_api_grants.sql qui rend explicite l'accès à la Data API Supabase pour les nouveaux projets et les réinitialisations de base : les privilèges public-schema par défaut sont révoqués et les grants de table anon/authenticated/service_role prévus sont réappliqués, les miroirs de schéma complet portant la même surface de grants
Modèle de grants Data API documenté dans la doc de configuration Supabase, le guide d'onboarding, les notes de déploiement du README et les règles IA base de données afin que les futures tables publiques, vues, séquences et RPC livrent grants/revokes en même temps que les politiques RLS
Mise à jour des skills locaux de l'agent Supabase depuis supabase/agent-skills à 4e69c80 : ajout du skill général supabase et rafraîchissement de supabase-postgres-best-practices à la disposition references/ amont 1.1.1
Mise à jour des entrypoints des outils IA et des mirrors de skills/règles générés pour que les futurs travaux Supabase invoquent à la fois le skill Supabase produit et le skill des bonnes pratiques Postgres lorsque pertinent
v1.27Minor
États de chargement des tableaux de bord, lectures asynchrones des layouts protégés et handlers de jobs durcis
Nouvelle directive d'état de chargement par segment de route pour les tableaux de bord admin plateforme, admin organisation et privé garantissant un retour immédiat (squelette + spinner) pendant la résolution des Server Components ; le motif asynchrone des layouts protégés est documenté ; les handlers de jobs en arrière-plan sont durcis avec un ordre de suppression RGPD plus sûr et la récupération des miroirs d'abonnement Stripe.
Ajout d'une directive d'état de chargement par segment de route pour les pages admin plateforme, admin organisation et tableau de bord privé afin que les futurs travaux conservent un retour immédiat (squelette + spinner) pendant la résolution des données Server Component
Documentation du motif asynchrone des tableaux de bord protégés : lancer les promesses du contexte de tableau de bord et des traductions en parallèle, attendre le contexte pour les redirections, puis attendre les traductions pour le rendu sans déplacer les données utilisateur/compte dans unstable_cache()
Mirrors des règles IA générées synchronisés pour la nouvelle directive de chargement et de lecture asynchrone des tableaux de bord
Durcissement des handlers de jobs en arrière-plan : la suppression RGPD vérifie désormais les erreurs de mutation Supabase, conserve les identifiants de retry jusqu'à ce que le travail destructif réussisse, et supprime les lignes de compte en dernier ; la synchronisation Stripe crée les miroirs d'abonnement manquants lorsque les webhooks de création ont été ratés
v1.26Minor
Droits à la vie privée, surfaces de conformité régionale et documentation
Nouvelle page /[locale]/privacy-choices et section vie privée/droits sur les données dans Mon compte exposant le droit d'opposition à la vente/au partage, l'opt-out publicité ciblée, le RGPD, la LPD suisse, les lois US des États, la LPRPDE canadienne et la LCAP ; le consentement cookies honore désormais les signaux Global Privacy Control du navigateur ; règles de sécurité canoniques, mirrors des règles IA et docs produit rafraîchis.
Nouvelle page /[locale]/privacy-choices et lien dans le footer exposant le droit d'opposition à la vente/au partage, l'opt-out publicité ciblée, le RGPD, la LPD suisse, les lois US des États, la LPRPDE canadienne et la LCAP
Nouvelle section vie privée/droits sur les données dans Mon compte avec point d'entrée d'export, gestion du consentement cookies, résumé des droits régionaux, accès à la politique de confidentialité, contact vie privée et contexte de suppression
Privacy Choices référencée depuis les pages CGU, Politique de confidentialité et Mentions légales existantes pour permettre la navigation entre toutes les surfaces légales/vie privée
/privacy-choices ajoutée au sitemap et à la liste des slugs CMS réservés afin que la route soit découvrable et ne puisse pas être masquée par des pages CMS dynamiques
Textes légaux/footer/cookies/compte localisés pour en-US, en-CA, fr-FR et fr-CH
Le consentement cookies honore désormais les signaux Global Privacy Control du navigateur — le consentement marketing/partage/publicité ciblée reste désactivé même quand 'Tout accepter' est cliqué
L'UI des paramètres cookies affiche l'état GPC actif et désactive le consentement marketing tant que ce signal est présent
Règles de sécurité canoniques mises à jour pour préserver /privacy-choices et le comportement Global Privacy Control dans les changements futurs
Mirrors générés des règles IA synchronisés pour Codex, Gemini et OpenCode
Docs produit mises à jour pour RGPD/vie privée, consentement cookies, analytics, Mon compte, onboarding production, README et le changelog
v1.25Minor
Hydratation auth marketing, déduplication des requêtes dashboard et docs
Les pages marketing publiques restent anonymes-first et cacheables tandis que l'UI authentifiée s'hydrate côté client via /api/auth/me ; les dashboards privé et org partagent désormais un contexte mis en cache par requête avec des RPCs SQL d'agrégation et des index composites ; règles canoniques caching/auth et docs produit rafraîchies.
Le layout marketing public (app/[locale]/(frontend)/layout.tsx) ne lit plus getUser() / les données de session Supabase côté serveur pour la personnalisation du navbar — reste anonymous-first et cacheable
Nouvel endpoint GET /api/auth/me pour l'hydratation auth côté client de la shell marketing : apiSecurity.public({ rateLimit: 'relaxed' }), getUser() côté serveur, payloads auth/profil/workspace minimaux et sanitisés, et Cache-Control: private, no-store
Nouveaux MarketingAuthProvider et MarketingCommandPalette ; TransparentNavbar et AccountDropdown consomment désormais le contexte auth hydraté côté client — les utilisateurs connectés cliquant sur le lien login anonyme sont toujours redirigés vers leur dashboard par le guard de route auth existant
Nouveau core/accounts/dashboard-context.ts avec helpers React.cache() au niveau de la requête : getPrivateDashboardContext() pour /private-dashboard/* et getOrgDashboardContext() pour /org-dashboard/*
Le layout/pages du dashboard privé (private-dashboard, chat, documents, referrals, affiliates) et le layout/pages du dashboard org (overview/admin, analytics, billing, API keys, members, roles, détail membre, settings) réutilisent désormais le contexte auth/account/workspace partagé au lieu de répéter les lectures getUser(), profil, membership, billing-access et subscription
Les dashboards protégés restent dynamiques et scopés à la requête ; aucune donnée spécifique à l'utilisateur n'a été déplacée vers unstable_cache()
Nouveaux RPCs d'agrégation dashboard pour les counts overview privé, l'analytics org et les résumés d'usage par membre, plus des index composites pour les hot paths dashboard sur ai_requests, chat_sessions et user_access_logs — les pages analytics/members org agrègent désormais en SQL au lieu de tirer les lignes d'usage brutes dans les Server Components
Composants client de purchase de crédits et end-trial chargés en lazy dans les sidebars/actions du dashboard, pour que le code UI modal/paiement ne fasse plus partie du bundle client initial du dashboard
Règles canoniques de caching mises à jour pour documenter l'hydratation auth marketing, les attentes rate-limit / Cache-Control de /api/auth/me, et la déduplication du contexte dashboard
Docs produit sous content/docs rafraîchies pour caching/performance, authentification, référence API et dashboards admin
v1.24Minor
Route-thinning, CI + guardrails, durcissement sécurité post-1.23 & docs v2
Suite à 1.23 : passe de route-thinning de l'API déléguant la logique multi-étapes vers core/*, nouveau workflow GitHub Actions CI avec scripts guardrail et lint gate zéro-warning, durcissement sécurité TRUST_CLOUDFLARE_IP / recent-auth / attribution RLS, refonte de la doc multi-pages public/docs/v2/, et refresh des règles et entrypoints des outils IA.
Nouveau gate d'env TRUST_CLOUDFLARE_IP : le rate limiting ne fait plus confiance au header CF-Connecting-IP spoofable — il n'est honoré que si le gate est explicitement activé
Le webhook Stripe lit le body brut en stream avec contrôle de taille au lieu de bufferiser tout arrayBuffer() d'abord
/api/jobs/run sépare proprement le chemin cron à bearer-token du chemin admin-session, avec contrôles rate-limit, CSRF et body-size équivalents
Le désabonnement newsletter cible désormais par défaut uniquement l'utilisateur authentifié — plus de désabonnement anonyme d'email arbitraire
Recent-auth + admin_logs immutables étendus à l'annulation/réactivation d'abonnement, aux purges de logs, aux crédits accordés à des utilisateurs et aux mutations d'organisation
Settings org, révocation d'invitation, écritures de profil onboarding et mises à jour de profil /my-account déplacés derrière des frontières server/API validées ; RLS chat/document scoping par compte durcie pour bloquer toute attribution user_id forgée
Passe de route-thinning de l'API : export GDPR, orchestration AI stream, pages/médias/blocs CMS, settings/users/organizations/subscriptions admin et clés API de compte délèguent désormais la logique multi-étapes à core/gdpr, core/ai, core/cms, core/admin, core/organizations, core/billing et core/accounts — aucun app/api/**/route.ts ne dépasse 250 lignes
Splits de routes à haut risque finalisés : dispatch webhook Stripe dans core/billing/stripe-webhook.ts, création de session de checkout dans core/billing/checkout.ts, auth/exécution job-run dans core/jobs/run-request.ts et helpers body / security-response bornés dans lib/http/request-body.ts — les routes ne sont plus que glue HTTP/sécurité
Nouveau .github/workflows/ci.yml exécuté sur les pull requests et les pushes vers main : typecheck, lint:ci, lint:strict, guardrails projet, couverture pattern API, taille de route API, couverture schéma/policy RLS, couverture des types Supabase, tests unitaires, scan de secrets, build production, accessibilité et checks responsive
Scripts guardrail nouveaux ou étendus : anti-patterns projet, couverture wrapper de sécurité API, taille de route API, couverture RLS sur schema.sql + cms-schema.sql, couverture schéma/types Supabase, sonde staging readiness et load smoke test des APIs critiques
lint:strict est désormais un CI gate zéro-warning ; la visibilité historique des warnings vit dans npm run lint:debt (4 787 warnings mesurés pendant cette passe)
Nouvelle documentation statique multi-pages dans public/docs/v2/ avec navigation partagée, styling responsive, sidebar à icônes groupées, menus de sous-section par page, assets public/docs/screens/ réutilisés, callouts visuels enrichis et un search-index.json global couvrant setup, configuration, architecture, Supabase, auth/B2B, pricing Stripe, AI/RAG, ops, contenu, frontend, sécurité, déploiement, référence API et outils IA
public/docs/index.html refait pour l'onboarding du premier projet : quick-start plus clair, checklist de configuration nouveau projet, guidance d'env par scénario, setup explicite config/pricing.ts / Price IDs Stripe, notes B2B checkout-before-onboarding et auto-bootstrap workspace, comptes de skills AI Blueprint mis à jour, et références à des assets locaux périmés supprimées
Les règles IA canoniques et /docs documentent désormais le split de maintenabilité admin-dashboard / billing (core/admin/*, modules core/billing/* focalisés, hooks de réversion affiliate/referral dans risk-events.ts), le contrat d'env TRUST_CLOUDFLARE_IP (ajouté aussi au .env.local généré par init), le désabonnement authentifié, l'API jobs durcie, les exigences recent-auth admin, les frontières RLS/écriture profil plus strictes, l'expansion privacy GDPR/US/Canada/Suisse (locale fr-CH, pricing CHF, copy légale suisse, seeds CMS légaux) et les contrats d'env du lint gate zéro-warning / lint:debt / couverture RLS / sonde staging / load smoke — les mirrors de règles IA générés sont rafraîchis via scripts/sync-ai-rules.sh
Ajout de .impeccable.md comme handoff de contexte de design projet ; nouveaux scripts CI/guardrail documentés dans les entrypoints des outils IA, les règles canoniques, README.md et la doc de tests produit
v1.23Minor
Remédiation d'audit, billing, docs, email & skills IA consolidés
Correctifs issus de l'audit complet du codebase (foundingCodex.md / planFix.md) : tracking privacy-by-default, preuve de suppression GDPR/LPD, complétude des exports, corrections billing/pricing, contrat d'env légal, parité de docs entre outils IA, refonte des emails transactionnels et refresh de /ui-ux-pro-max en v2.5.0.
Opérateurs : appliquer 4 nouvelles migrations sur la base de prod — durcissement DB planfix, index subscriptions (hors transaction), statut des processeurs de suppression, et normalisation des clés de locale CMS (fr/en → fr-FR/en-US) ; mirrorées dans supabase/schema.sql et supabase/cms-schema.sql
Analytics et live-chat désormais privacy-by-default : GTM, gtag/Google Ads, Meta Pixel, X Pixel et Crisp ne se chargent ou ne se déclenchent qu'après le consentement analytics/marketing correspondant ; events purchase/signup gardés au niveau applicatif
Persistance du consentement utilisateur connecté passe désormais par POST /api/user/consents avec apiSecurity.authenticated() + CSRF + Zod + rate limiting ; les composants navigateur n'écrivent plus directement dans user_consents
Uploads CMS WYSIWYG : token CSRF double-submit envoyé et rate limits media-appropriés ; uploads RAG conservent auth + CSRF + body-size sur la limite standard interactive
Remédiation planFix.md : 63/64 findings corrigés — garde fail-closed auth.role() sur les RPCs SECURITY DEFINER referral/affiliate, lectures de hash de clés API révoquées aux rôles anon/authenticated, RLS UPDATE de app_settings dotée d'un WITH CHECK
Vérification admin et escalation durcies : cookie recent-auth 60 s, bucket de rate-limit OTP par email, rejet is_disabled dans verify-OTP et callback magic-link, recent-auth sur admin_email et toggle_admin, audit logs pour les actions destructrices
Export GDPR élargi — profil, memberships, sessions/messages de chat, requêtes IA, consentements, payments, abonnements, licences, invitations, access logs, demandes de suppression, préférences de notification, emails en attente, métadonnées de documents et de clés API (sans secrets ni hashes)
process-account-deletions enregistre le statut des processeurs pour Stripe, newsletter/email, Supabase Storage, cascade DB et Supabase Auth ; échecs rédigés, retry_count incrémenté, requête marquée failed et complétion factice bloquée ; le nettoyage Storage des documents/avatars se fait avant suppression des lignes
Nouveau contrat d'env légal : LEGAL_COMPANY_NAME, LEGAL_ADDRESS, LEGAL_REGISTRATION_NUMBER — npm run init, .env.example, config/app.ts et /docs câblés ; les placeholders d'exemple bloquent intentionnellement les builds publics/indexables
npm run init se termine désormais par un guide Pricing & Stripe — config/pricing.ts comme source de vérité, mapping des Price IDs, choix du modèle de billing, webhooks et moyens de paiement
Fin anticipée d'un trial Stripe synchronise le miroir local des subscriptions avant de répondre (l'org-dashboard retire la bannière trial immédiatement) ; les crédits de conversion de trial soustraient le grant trial configuré de l'allocation du plan au lieu d'ajouter le montant total
Le warning d'expiration de licence utilise la lookup owner en deux étapes documentée au lieu de l'embed invalide profiles!owner_user_id — corrige la régression de zéro email en prod
Détection de patterns d'attaque contact/newsletter câblée ; code helper CSRF non utilisé supprimé ; les checks CSRF Origin/Referer utilisent appConfig.url ; l'allowlist d'iframes HTML est désormais dans DOMPurify
Les tables de pricing affichent les crédits inclus, les limites de projets et les crédits trial depuis config/pricing.ts / variables trial au lieu de duplications de traductions de feature values
Lookup de locale CMS/page/blog/changelog tombe sur des clés langue-seule legacy en fallback ; les écritures admin normalisent les payloads vers les clés BCP-47 ; seeds, sitemap et npm run init utilisent les locales configurées au lieu de fr / en
Docs outils IA synchronisés entre .claude/.codex/.gemini/.opencode/.agents — 22 fichiers par mirror ; les entrypoints des agents incluent matrice de routing, sélection reviewer/skill et pointeurs de vérification par type de changement ; scripts/sync-ai-rules.sh régénère .agents/skills/ depuis .claude/skills/
Extractions thin-route + core/ pour streaming/accounting IA, changelog, mutations chat, billing subscribe-free, admin orgs/users et validation CMS ; core/usage/ et core/memberships/ morts supprimés
CURRENCY_CODES est désormais le tuple canonique des devises (Currency et SUPPORTED_CURRENCIES en dérivent) ; ids de modèles d'agents, sel de referral, bonus signup workspace, seuil RAG, alerte low-credit, fallbacks locale/devise et rôles org-manager utilisent des helpers de config
Les builders d'emails transactionnels partagent lib/email/layout.ts : shell monochrome, layout en tables, CSS dark-mode, fallback CTA bulletproof et helpers d'escape ; gradients par template supprimés ; couverts par __tests__/email/templates-render.test.ts
Le contrôle de body-size de /api/ai/stream lit le body cloné en stream au lieu de bufferiser tout arrayBuffer() d'abord
Traitement RAG, sync Stripe, processeurs GDPR de suppression, lectures d'export, réordonnancement de rôles, revalidation affiliate et purges de logs parallélisés ou batchés ; select('*') remplacés par des projections explicites sur les hot paths flaggés
Next.js mis à jour en 16.2.6 ; npm run audit:bundle utilise next build --webpack car @next/bundle-analyzer n'émet pas de rapports sous Turbopack (rapports dans .next/analyze/)
Clés de rate-limit IPv6 ramenées au préfixe /64 (contournement fermé), X (Twitter) Pixel sous consentement, et passe correctness + responsive sur /docs.
Contournement du rate-limit IPv6 fermé — clés ramenées au préfixe réseau /64 (RFC 4291) en un point unique dans createRateLimiter() ; IPv4 inchangé
normalizeRateLimitIdentifier() ne lève jamais d'erreur et laisse les formes IPv4-mapped intactes ; getClientIP() renvoie toujours le /128 complet pour les chemins contact et IP-hash
Nouveau test unitaire __tests__/security/rate-limit-ip.test.ts : expansion ::, collapse de préfixe, pass-through IPv4 et entrées malformées
X (Twitter) Pixel via components/analytics/x-pixel.tsx — rendu uniquement si consent.marketing est accordé, même gate que le Meta Pixel
xEvents.purchase / xEvents.signUp déclenchés depuis les chemins existants trackPurchase / trackSignUp ; no-op si les variables d'event de conversion sont absentes
Trois variables NEXT_PUBLIC_X_PIXEL_* optionnelles, validées par Zod ; static.ads-twitter.com ajouté à l'allowlist CSP
Page /docs autonome : suppression de deux sections de version obsolètes, ajout d'un lien Changelog vers la source unique de vérité
Tables /docs : clipping mobile corrigé (13 tables wrappées), offset d'ancre, lazy-loading des images, style pre répété dé-inliné
v1.21Patch
Moyens de paiement Stripe Checkout
PayPal activé explicitement pour les paiements uniques via une source de config unique ; les abonnements restent en carte seule (restriction géo Stripe).
PayPal apparaît désormais sur Stripe Checkout pour les paiements uniques — payment_method_types demandé explicitement au lieu de l'auto-sélection Stripe
Nouveau pricingConfig.checkoutPaymentMethods dans config/pricing.ts, indexé par mode Stripe (subscription : card, payment : card + paypal)
Union littérale CheckoutPaymentMethod dans lib/pricing/types.ts — une faute de frappe fait échouer npm run build avant le déploiement
Les abonnements restent en carte seule par défaut : Stripe limite les abonnements PayPal aux marchands EEE/UK/CH
Wallets sur carte (Apple Pay / Google Pay) inchangés — ils passent par l'entrée carte
npm run init rappelle d'activer les moyens dans Stripe Dashboard en test et en live
Docs mises à jour : .claude/rules/billing.md et mirrors, registre des sources de config domain-map
v1.20Minor
Refonte UX du lien magique
Code à 6 chiffres en repli, redirection cross-onglet, cooldown de renvoi et refonte des textes pour réduire l'abandon du lien magique.
Code OTP à 6 chiffres présent dans l'email du lien magique, à côté du lien ; longueur configurable via OTP_LENGTH (défaut 6, borné 6-10)
Nouvel endpoint POST /api/auth/verify-otp — validé par Zod, apiSecurity.public() + rate limit strict, erreur générique (pas d'énumération)
L'écran de succès affiche l'email de destination, un champ OTP numérique (autoComplete one-time-code) et un bouton retour Mauvais email ?
Redirection cross-onglet : l'écran de succès interroge getUser() toutes les 4s (en pause si l'onglet est masqué) ; cliquer le lien dans un autre onglet redirige l'original
Cooldown de renvoi (compte à rebours 30s) et rappel dossier spam
Refonte des textes : Continuer avec l'email, sous-titre mentionnant lien + code, sujet d'email Votre code de connexion
9 nouvelles clés auth.login.* et 3 email.magicLink.* dans les deux locales (test de parité vert)
Le script d'init demande OTP_LENGTH (validé) ; avertissement opérateur : doit correspondre au réglage Email OTP length du projet Supabase
v1.19Minor
Verrou de connexion prelaunch & catalogue OAuth dynamique
Allowlist admin côté serveur verrouillant la connexion en prelaunch, plus un catalogue de boutons OAuth multi-fournisseurs configurable.
La connexion en prelaunch est verrouillée côté serveur par PRELAUNCH_ALLOWED_EMAILS (server-only, séparée par virgules, insensible à la casse ; vide = verrouillé)
Trois points d'application : 403 lien magique avant generateLink, sign-out callback, sign-out OAuth + nettoyage admin.deleteUser
Message de restriction générique — aucun vecteur d'énumération d'email
Catalogue OAuth dynamique : NEXT_PUBLIC_OAUTH_PROVIDERS rend un bouton de marque par id, validé contre les 19 fournisseurs supportés par Supabase
Nouveau lib/auth/oauth-providers.tsx (SVG de marque inlinés, aucune dépendance ajoutée) + components/auth/oauth-buttons.tsx
login-form.tsx : bouton Google en dur supprimé ; le séparateur n'apparaît que si au moins un fournisseur est configuré
i18n : auth.login.continueWithProvider avec placeholder {provider} + auth.login.prelaunchRestricted, deux locales (parité verte)
Le script d'init réhydrate les deux variables sur les re-runs ; sections OAuth + prelaunch de public/docs/index.html réécrites
v1.18Minor
Programme d'affiliation & abstraction email
Programme partenaire orienté compte avec commissions en cash (Stripe Connect différé) et une couche email pluggable (Brevo, Mailjet, noop).
Correctif critique pg_net args positionnels (fuite de secret + tempête 401) et nouvelle surface admin pour piloter cron.job.
CRITIQUE : correctif args positionnels pg_net — JOBS_SECRET_KEY était envoyé en query string et fuyait dans les logs d'accès à chaque tick cron. Rotation du secret obligatoire après mise à jour.
Les cron jobs faisaient silencieusement 401 — net.http_post est maintenant appelé en arguments nommés, robuste à toutes les versions de pg_net
Dashboard admin Cron en /admin-dashboard/jobs/cron — list, toggle, unschedule, purge des orphelins
Tokens Bearer présents dans cron.job.command redactés au niveau SQL — jamais exposés à l'API/UI
Garde recent-auth sur les actions destructrices (unschedule, purge-orphans) ; audit trail sur chaque mutation
Script init : blocs de config security + logs restaurés (les init fraîches compilent à nouveau)
Script init : étape de purge des orphelins avant re-sync, filtre ancré (ne touche pas aux entrées cron tierces)
Helper require-recent-auth partagé, factorisé depuis le check inline de promotion admin
v1.15Minor
Réalignement de la comptabilité des crédits IA
Les crédits correspondent désormais 1:1 aux tokens LLM réellement consommés. Les forfaits sont recalibrés en conséquence.
1 crédit = 1 token LLM, en chat comme en RAG (fini les divergences réserve/remboursement)
Helper de formatage compact des crédits (1M, 2,5M, 500K) localisé FR/EN
Forfaits recalibrés à l'échelle token : Free 50K, Pro 1M, Business 5M (prix Stripe inchangés)
Champ téléphone migré vers react-international-phone (~250 pays, recherche, drapeaux)
/my-account résout maintenant accountId côté serveur — fini les cartes Abonnement / Parrainage silencieusement cassées
Carte Parrainage sur /my-account avec tuiles de stats et lien vers le tableau de bord complet
Le widget Crisp est désormais limité à la landing page (plus de propagation SPA vers privé/admin)
Visibilité de l'entrée Parrainages dans la sidebar admin corrigée (flag passé côté serveur)
v1.14Minor
Système de parrainage
Programme de parrainage end-to-end attaché aux comptes, avec rate limit IP et reversement automatique sur litige.
Codes de parrainage par compte — les deux parties gagnent des crédits sur déclencheur configuré
Garde-fous DB stricts : pas d'auto-parrainage, un parrainage par compte à vie, rate limit IP côté apply
Tableau de bord utilisateur sur /private-dashboard/referrals avec historique paginé
Console admin : KPI, top-10 parrains, liste filtrable, détail timeline, gestion des codes
Bannière d'inscription + auto-attribution depuis le cookie httpOnly à l'onboarding
Hooks webhook Stripe : qualification à l'achat, reversement automatique sur remboursement / litige perdu
404 partout quand REFERRAL_ENABLED=false — surface invisible tant que la fonctionnalité est désactivée
Audit du blueprint IA : 19 règles, 11 agents spécialisés, 6 hooks, catalogue d'anti-patterns
v1.13Patch
Suite de la revue de codebase
Corrections critiques à mineures issues de la revue parallèle multi-agents, sur 4 niveaux de sévérité.
Drift de schéma corrigé — migrations index + RLS de janvier 2026 reportées dans schema.sql
Webhook Stripe extrait : passage de 1 632 à ~160 lignes + fonctions apply* réutilisables
CSRF Double-Submit + CSP basée sur nonce en production (plus de unsafe-inline)
Défense anti-DNS-rebinding sur les webhooks de jobs — résolution IP rejetant les plages privées
Latence du stream IA : ~60–100 ms gagnés par requête (lookup auth dupliqué supprimé)
Listing blog : jusqu'à ~1 Mo économisé au cold load en supprimant le JSONB content/seo_*
MarkdownRenderer passé en Server Component — ~100 Ko retirés du bundle changelog/CMS
Prompt caching Anthropic activé — les prompts système longs bénéficient de la remise read 0,1×
v1.12Patch
Corrections SEO & sitemap
Sitemap complété, données structurées corrigées et métadonnées peaufinées sur tous les types de pages.
Fuite de pages brouillon dans le sitemap corrigée (filtre published manquant après passage au service client)
/contact, /blog, /changelog et les billets de blog apparaissent enfin dans le sitemap
Les 10 types de pages émettent maintenant un hreflang x-default
Suppression du SearchAction cassé du schéma WebSite (route /search inexistante)
Pages CMS : type OG corrigé en website, noindex/nofollow respectés, canonical + og_image supportés
Fallback de description pour métadonnées SEO vides (excerpt → 160 premiers caractères du contenu)
Blog : URLs filtrées/paginées (?page=, ?category=, ?tag=) émettent désormais noindex,follow
v1.11Minor
Skill UI/UX Pro Max
Décisions UI/UX assistées par IA avec bases de données curées et règles de raisonnement.