0.13.20 : check environment variables at boot
Build release Docker image / Build Docker Images (push) Successful in 7m58s

Trois erreurs de configuration successives — clés R2 pointant sur
l'ancien MinIO, `wwhsec_` au lieu de `whsec_`, `k_test_` amputé de son
`s` — n'ont produit aucun message exploitable. Chacune se manifestait
très loin de sa cause : un upload qui échoue, un webhook rejeté.

src/env-check.ts vérifie au bootstrap la forme des variables sensibles
(Stripe, R2, SMTP) : présence, préfixe, longueur, et les altérations de
copier-coller (guillemets englobants, espaces parasites). Les trois
erreurs ci-dessus sont détectées.

Ne bloque pas le démarrage — une clé SMTP erronée ne doit pas empêcher
le site de servir — mais journalise en erreur, une ligne par anomalie.
Aucune valeur n'est journalisée, uniquement des longueurs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 01:23:51 +02:00
parent 57faa37e70
commit c0a0966557
3 changed files with 172 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "harmony-back",
"version": "0.13.19",
"version": "0.13.20",
"private": true,
"description": "A Strapi application",
"scripts": {
+164
View File
@@ -0,0 +1,164 @@
import type { Core } from "@strapi/strapi";
/**
* Contrôle des variables d'environnement au démarrage.
*
* Motivation : trois erreurs de configuration successives (clés R2 pointant
* sur l'ancien MinIO, `wwhsec_` au lieu de `whsec_`, `k_test_` amputé de son
* `s`) n'ont produit aucun message exploitable. Chacune s'est manifestée très
* loin de sa cause — un upload qui échoue, un webhook rejeté — et a coûté des
* heures de diagnostic.
*
* Ce module ne vérifie que ce qui est vérifiable sans appel réseau : présence,
* préfixe, longueur, et les altérations de copier-coller (guillemets
* englobants, espaces parasites). Il ne valide évidemment pas qu'une clé est
* la *bonne* — seulement qu'elle a la forme attendue.
*
* Il ne bloque pas le démarrage : une clé SMTP erronée ne doit pas empêcher
* le site de servir. Il journalise en erreur, ce qui suffit à orienter le
* diagnostic dès le boot.
*
* ⚠️ Aucune valeur n'est journalisée, jamais — uniquement des longueurs.
*/
type EnvRule = {
name: string;
/** Préfixe attendu, le cas échéant. */
prefix?: string;
/** Longueur exacte attendue. */
length?: number;
/** Longueur minimale attendue. */
minLength?: number;
/** Contexte affiché dans le message, pour orienter la correction. */
usedFor: string;
};
const ENV_RULES: EnvRule[] = [
// --- Paiement
{
name: "STRIPE_SECRET_KEY",
prefix: "sk_",
minLength: 20,
usedFor: "appels à l'API Stripe",
},
{
name: "STRIPE_WEBHOOK_SECRET",
prefix: "whsec_",
minLength: 30,
usedFor: "vérification de signature des webhooks Stripe",
},
// --- Stockage des médias (Cloudflare R2)
{
name: "R2_ACCESS_KEY_ID",
length: 32,
usedFor: "upload des médias",
},
{
name: "R2_SECRET_ACCESS_KEY",
length: 64,
usedFor: "upload des médias",
},
{
name: "R2_ENDPOINT",
prefix: "https://",
usedFor: "upload des médias",
},
{
name: "R2_BUCKET",
minLength: 1,
usedFor: "upload des médias",
},
{
name: "R2_PUBLIC_URL",
prefix: "https://",
usedFor: "URLs publiques des médias",
},
// --- Email transactionnel
{
name: "SMTP_PASSWORD",
minLength: 10,
usedFor: "envoi des emails transactionnels",
},
];
/** Guillemets englobants ou espaces parasites laissés par un copier-coller. */
const findCopyPasteDamage = (raw: string): string | null => {
if (raw !== raw.trim()) {
return "espace ou retour à la ligne en début ou fin de valeur";
}
const quoted =
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"));
if (quoted && raw.length > 1) {
return "valeur entourée de guillemets (ils font partie de la valeur)";
}
return null;
};
const checkRule = (rule: EnvRule): string[] => {
const raw = process.env[rule.name];
if (raw === undefined || raw === "") {
return [`${rule.name} n'est pas définie — ${rule.usedFor}`];
}
const problems: string[] = [];
const damage = findCopyPasteDamage(raw);
if (damage) {
problems.push(`${rule.name} : ${damage}`);
}
const value = raw.trim();
if (rule.prefix && !value.startsWith(rule.prefix)) {
problems.push(
`${rule.name} ne commence pas par « ${rule.prefix} » — ${rule.usedFor}`,
);
}
if (rule.length !== undefined && value.length !== rule.length) {
problems.push(
`${rule.name} fait ${value.length} caractères, ${rule.length} attendus — ${rule.usedFor}`,
);
}
if (rule.minLength !== undefined && value.length < rule.minLength) {
problems.push(
`${rule.name} fait ${value.length} caractères, au moins ${rule.minLength} attendus — ${rule.usedFor}`,
);
}
return problems;
};
/**
* Vérifie la forme des variables d'environnement et journalise les anomalies.
* @returns le nombre d'anomalies détectées
*/
export const checkEnvironment = (strapi: Core.Strapi): number => {
const problems = ENV_RULES.flatMap(checkRule);
if (problems.length === 0) {
strapi.log.info(
`[env-check] ${ENV_RULES.length} variables vérifiées, aucune anomalie de forme`,
);
return 0;
}
strapi.log.error(
`[env-check] ${problems.length} anomalie(s) détectée(s) dans les variables d'environnement :`,
);
for (const problem of problems) {
strapi.log.error(`[env-check] ✗ ${problem}`);
}
strapi.log.error(
"[env-check] Ces valeurs viennent de l'environnement de déploiement " +
"(Dokploy en production, .env en local). Une variable modifiée n'est " +
"relue qu'au redémarrage du processus.",
);
return problems.length;
};
+7
View File
@@ -1,4 +1,5 @@
import type { Core } from "@strapi/strapi";
import { checkEnvironment } from "./env-check";
import { syncRolePermissions } from "./permissions-sync";
export default {
@@ -26,6 +27,12 @@ export default {
* run jobs, or perform some special logic.
*/
async bootstrap({ strapi }: { strapi: Core.Strapi }) {
// Contrôle de forme des variables d'environnement (préfixes, longueurs,
// guillemets parasites). Ne bloque pas le démarrage, mais signale au boot
// les erreurs de configuration qui, sinon, ne se manifestent que très
// loin de leur cause — cf. src/env-check.ts.
checkEnvironment(strapi);
// Synchronise les permissions Users & Permissions (source de vérité
// versionnée dans src/permissions-sync.ts — additif, idempotent).
await syncRolePermissions(strapi);