diff --git a/docker-compose.yml b/docker-compose.yml index fa93603..201ecc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,7 @@ services: # Passerelle centralisée : le jeton reste dans l'environnement serveur. REFERENTIEL_IA_URL: ${REFERENTIEL_IA_URL:-} REFERENTIEL_IA_TOKEN: ${REFERENTIEL_IA_TOKEN:-} + REFERENTIEL_IA_TOKEN_FILE: /run/secrets/referentiel_ia_token UPLOAD_DIR: /app/uploads # Chemin unique utilisé par le serveur et vérifié au démarrage. STORAGE_BASE_PATH: /app/storage @@ -81,6 +82,10 @@ services: target: /app/storage volume: nocopy: true + secrets: + - source: referentiel_ia_token + target: referentiel_ia_token + mode: 0400 # L’application doit utiliser eth0 sur le réseau de données ; l’ordre évite # toute inversion d’interface lors d’une recréation Docker multi-réseaux. networks: @@ -110,6 +115,10 @@ volumes: external: true name: ${STORAGE_VOLUME_NAME:-demat-facturation-dsi_demat_facturation_storage} +secrets: + referentiel_ia_token: + file: /opt/manus-secrets/demat-facturation-referentiel-ia-token + networks: demat-facturation-internal: driver: bridge diff --git a/server/_core/env.ts b/server/_core/env.ts index c887e35..c2f52fc 100644 --- a/server/_core/env.ts +++ b/server/_core/env.ts @@ -1,3 +1,15 @@ +import { readFileSync } from "node:fs"; + +/** Lit un secret monté par Docker, sans jamais exposer son contenu aux logs. */ +export function readSecretFile(path: string | undefined): string { + if (!path) return ""; + try { + return readFileSync(path, "utf8").trim(); + } catch { + return ""; + } +} + export const ENV = { appId: process.env.VITE_APP_ID ?? "", cookieSecret: process.env.JWT_SECRET ?? "", @@ -10,5 +22,5 @@ export const ENV = { mistralApiKey: process.env.MISTRAL_API_KEY ?? "", geminiApiKey: process.env.GEMINI_API_KEY ?? "", referentielIaUrl: process.env.REFERENTIEL_IA_URL ?? "", - referentielIaToken: process.env.REFERENTIEL_IA_TOKEN ?? "", + referentielIaToken: process.env.REFERENTIEL_IA_TOKEN ?? readSecretFile(process.env.REFERENTIEL_IA_TOKEN_FILE), }; diff --git a/server/deploymentRouting.test.ts b/server/deploymentRouting.test.ts index 6788439..b810bbd 100644 --- a/server/deploymentRouting.test.ts +++ b/server/deploymentRouting.test.ts @@ -41,5 +41,7 @@ describe("routage et persistance de déploiement", () => { expect(composeFile).toContain("REFERENTIEL_IA_URL: ${REFERENTIEL_IA_URL:-}"); expect(composeFile).toContain("REFERENTIEL_IA_TOKEN: ${REFERENTIEL_IA_TOKEN:-}"); + expect(composeFile).toContain("REFERENTIEL_IA_TOKEN_FILE: /run/secrets/referentiel_ia_token"); + expect(composeFile).toContain("file: /opt/manus-secrets/demat-facturation-referentiel-ia-token"); }); }); diff --git a/server/referentialIa.test.ts b/server/referentialIa.test.ts index 1917660..27d0469 100644 --- a/server/referentialIa.test.ts +++ b/server/referentialIa.test.ts @@ -1,9 +1,13 @@ import { describe, expect, it } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { invokeReferentialIa, testReferentialIaConnection, type ReferentielIaConnection, } from "./referentialIa"; +import { readSecretFile } from "./_core/env"; const connection: ReferentielIaConnection = { baseUrl: "https://referentiel-ia.recette.santinova-soft.org", @@ -18,6 +22,18 @@ function jsonResponse(body: unknown, status = 200) { } describe("Référentiel IA", () => { + it("lit un jeton serveur depuis un fichier monté sans le journaliser", () => { + const directory = mkdtempSync(join(tmpdir(), "referentiel-ia-")); + const secretFile = join(directory, "token"); + try { + writeFileSync(secretFile, " server-only-token\n", { mode: 0o600 }); + expect(readSecretFile(secretFile)).toBe("server-only-token"); + expect(readSecretFile(join(directory, "absent"))).toBe(""); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + it("transmet une requête OpenAI-compatible à la passerelle sélectionnée", async () => { let receivedUrl = ""; let receivedInit: RequestInit | undefined;