{"repo": "amineakik/sentinel-fullstack", "files": {"docker-compose.yml": {"492826051c9013f6780aabba286e0954d6a2586f": {"t": 1785745268.1707659, "n": 4224, "h": "6a38042ff679fd26", "c": "x-logging: &default-logging\n driver: json-file\n options:\n max-size: 10m\n max-file: '5'\n\nservices:\n postgres:\n image: postgres:15.18-alpine3.23@sha256:3889f6e66267065437b17a404058a6220d9080c73b701edd225770f8b2d6a52c\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']\n interval: 5s\n timeout: 5s\n retries: 10\n start_period: 10s\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n args:\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n init: true\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n - edge\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3000/api/health > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 20s\n environment:\n PORT: 3000\n NODE_ENV: production\n DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}\n COOKIE_SECRET: ${COOKIE_SECRET:?COOKIE_SECRET must be set}\n JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:?CLIENT_ORIGIN must be set}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:?BOARD_ACCESS_CODE_HASH must be set}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:?TRUST_PROXY must be set}\n LOG_LEVEL: ${LOG_LEVEL:-info}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-3000}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-900000}\n SUPPORT_API_TIMEOUT_MS: ${SUPPORT_API_TIMEOUT_MS:-20000}\n NOTIFICATION_BATCH_SIZE: ${NOTIFICATION_BATCH_SIZE:-10}\n NOTIFICATION_MAX_ATTEMPTS: ${NOTIFICATION_MAX_ATTEMPTS:-5}\n NOTIFICATION_POLL_INTERVAL_MS: ${NOTIFICATION_POLL_INTERVAL_MS:-5000}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n stop_grace_period: 30s\n logging: *default-logging\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-}\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:8080/ > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n caddy:\n image: caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648\n restart: unless-stopped\n ports:\n - '80:80'\n - '443:443'\n - '443:443/udp'\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:?CADDY_DOMAIN must be set}\n depends_on:\n frontend:\n condition: service_healthy\n backend:\n condition: service_healthy\n networks:\n - internal\n - edge\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\nnetworks:\n internal:\n internal: true\n edge:\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "ef9dca2f9eaad36b429af18ad45b2952c58207a0": {"t": 1785749422.9968348, "n": 4224, "h": "6a38042ff679fd26", "c": "x-logging: &default-logging\n driver: json-file\n options:\n max-size: 10m\n max-file: '5'\n\nservices:\n postgres:\n image: postgres:15.18-alpine3.23@sha256:3889f6e66267065437b17a404058a6220d9080c73b701edd225770f8b2d6a52c\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']\n interval: 5s\n timeout: 5s\n retries: 10\n start_period: 10s\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n args:\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n init: true\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n - edge\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3000/api/health > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 20s\n environment:\n PORT: 3000\n NODE_ENV: production\n DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}\n COOKIE_SECRET: ${COOKIE_SECRET:?COOKIE_SECRET must be set}\n JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:?CLIENT_ORIGIN must be set}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:?BOARD_ACCESS_CODE_HASH must be set}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:?TRUST_PROXY must be set}\n LOG_LEVEL: ${LOG_LEVEL:-info}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-3000}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-900000}\n SUPPORT_API_TIMEOUT_MS: ${SUPPORT_API_TIMEOUT_MS:-20000}\n NOTIFICATION_BATCH_SIZE: ${NOTIFICATION_BATCH_SIZE:-10}\n NOTIFICATION_MAX_ATTEMPTS: ${NOTIFICATION_MAX_ATTEMPTS:-5}\n NOTIFICATION_POLL_INTERVAL_MS: ${NOTIFICATION_POLL_INTERVAL_MS:-5000}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n stop_grace_period: 30s\n logging: *default-logging\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-}\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:8080/ > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n caddy:\n image: caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648\n restart: unless-stopped\n ports:\n - '80:80'\n - '443:443'\n - '443:443/udp'\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:?CADDY_DOMAIN must be set}\n depends_on:\n frontend:\n condition: service_healthy\n backend:\n condition: service_healthy\n networks:\n - internal\n - edge\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\nnetworks:\n internal:\n internal: true\n edge:\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "4ff629f41cf5e3e1ca8b77e6c0faee2c17c030b2": {"t": 1785749424.2240922, "n": 4080, "h": "d7bd3363f51bb18a", "c": "x-logging: &default-logging\n driver: json-file\n options:\n max-size: 10m\n max-file: '5'\n\nservices:\n postgres:\n image: postgres:15.18-alpine3.23\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']\n interval: 5s\n timeout: 5s\n retries: 10\n start_period: 10s\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n args:\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n init: true\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n - edge\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3000/api/health > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 20s\n environment:\n PORT: 3000\n NODE_ENV: production\n DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}\n COOKIE_SECRET: ${COOKIE_SECRET:?COOKIE_SECRET must be set}\n JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:?CLIENT_ORIGIN must be set}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:?BOARD_ACCESS_CODE_HASH must be set}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:?TRUST_PROXY must be set}\n LOG_LEVEL: ${LOG_LEVEL:-info}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-3000}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-900000}\n SUPPORT_API_TIMEOUT_MS: ${SUPPORT_API_TIMEOUT_MS:-20000}\n NOTIFICATION_BATCH_SIZE: ${NOTIFICATION_BATCH_SIZE:-10}\n NOTIFICATION_MAX_ATTEMPTS: ${NOTIFICATION_MAX_ATTEMPTS:-5}\n NOTIFICATION_POLL_INTERVAL_MS: ${NOTIFICATION_POLL_INTERVAL_MS:-5000}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n stop_grace_period: 30s\n logging: *default-logging\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-}\n BUILD_SHA: ${BUILD_SHA:?BUILD_SHA must be set to the deployed Git commit}\n restart: unless-stopped\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:8080/ > /dev/null || exit 1']\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n caddy:\n image: caddy:2.11.4-alpine\n restart: unless-stopped\n ports:\n - '80:80'\n - '443:443'\n - '443:443/udp'\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:?CADDY_DOMAIN must be set}\n depends_on:\n frontend:\n condition: service_healthy\n backend:\n condition: service_healthy\n networks:\n - internal\n - edge\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\nnetworks:\n internal:\n internal: true\n edge:\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "73e02322a5e1a91d429bc5e0ab75c4dbadab883a": {"t": 1785749425.3094294, "n": 4029, "h": "000627a66a7922a4", "c": "x-logging: &default-logging\n driver: json-file\n options:\n max-size: 10m\n max-file: '5'\n\nservices:\n postgres:\n image: postgres:15.18-alpine3.23\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n start_period: 10s\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n restart: unless-stopped\n init: true\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n - edge\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://127.0.0.1:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 20s\n environment:\n PORT: 3000\n NODE_ENV: production\n DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}\n COOKIE_SECRET: ${COOKIE_SECRET:?COOKIE_SECRET must be set}\n JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:?CLIENT_ORIGIN must be set}\n BOARD_ACCESS_LABEL: ${BOARD_ACCESS_LABEL:-Board atelier}\n BOARD_SESSION_TTL_HOURS: ${BOARD_SESSION_TTL_HOURS:-12}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:?BOARD_ACCESS_CODE_HASH must be set}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:?TRUST_PROXY must be set}\n LOG_LEVEL: ${LOG_LEVEL:-info}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-3000}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-900000}\n SUPPORT_API_TIMEOUT_MS: ${SUPPORT_API_TIMEOUT_MS:-20000}\n NOTIFICATION_BATCH_SIZE: ${NOTIFICATION_BATCH_SIZE:-10}\n NOTIFICATION_MAX_ATTEMPTS: ${NOTIFICATION_MAX_ATTEMPTS:-5}\n NOTIFICATION_POLL_INTERVAL_MS: ${NOTIFICATION_POLL_INTERVAL_MS:-5000}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n stop_grace_period: 30s\n logging: *default-logging\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-}\n restart: unless-stopped\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://127.0.0.1:8080/ > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n read_only: true\n tmpfs:\n - /tmp:rw,noexec,nosuid,size=64m\n cap_drop:\n - ALL\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\n caddy:\n image: caddy:2.11.4-alpine\n restart: unless-stopped\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:?CADDY_DOMAIN must be set}\n depends_on:\n frontend:\n condition: service_healthy\n backend:\n condition: service_healthy\n networks:\n - internal\n - edge\n security_opt:\n - no-new-privileges:true\n logging: *default-logging\n\nnetworks:\n internal:\n internal: true\n edge:\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "ad05cfcd9349b56b4b3f0bdc86da620a7c6f26a5": {"t": 1785749426.38969, "n": 2958, "h": "2a27c1983122043e", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER:-sentinel} -d ${POSTGRES_DB:-sentinel}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n restart: unless-stopped\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://localhost:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n NODE_ENV: ${NODE_ENV:-development}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n BOARD_ACCESS_LABEL: ${BOARD_ACCESS_LABEL:-Board atelier}\n BOARD_SESSION_TTL_HOURS: ${BOARD_SESSION_TTL_HOURS:-12}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:-}\n LOG_LEVEL: ${LOG_LEVEL:-}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:3000}\n container_name: sentinel_frontend\n restart: unless-stopped\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n\n caddy:\n image: caddy:2-alpine\n container_name: sentinel_caddy\n restart: unless-stopped\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost}\n depends_on:\n - frontend\n - backend\n networks:\n - internal\n\nnetworks:\n internal:\n driver: bridge\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "ea202b4fb16b31c3161e6dca0feb18875b64f9a8": {"t": 1785749428.491816, "n": 2846, "h": "f7b0b3a18ec1974e", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER:-sentinel} -d ${POSTGRES_DB:-sentinel}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://localhost:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n NODE_ENV: ${NODE_ENV:-development}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n BOARD_ACCESS_LABEL: ${BOARD_ACCESS_LABEL:-Board atelier}\n BOARD_SESSION_TTL_HOURS: ${BOARD_SESSION_TTL_HOURS:-12}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:-}\n LOG_LEVEL: ${LOG_LEVEL:-}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-}\n SMTP_HOST: ${SMTP_HOST:-}\n SMTP_PORT: ${SMTP_PORT:-587}\n SMTP_SECURE: ${SMTP_SECURE:-false}\n SMTP_USER: ${SMTP_USER:-}\n SMTP_PASS: ${SMTP_PASS:-}\n SMTP_FROM: ${SMTP_FROM:-}\n ADMIN_EMAIL: ${ADMIN_EMAIL:-}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:3000}\n container_name: sentinel_frontend\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n\n caddy:\n image: caddy:2-alpine\n container_name: sentinel_caddy\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost}\n depends_on:\n - frontend\n - backend\n networks:\n - internal\n\nnetworks:\n internal:\n driver: bridge\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "2c86938346a8decb0d735cd3c8fe61908a464664": {"t": 1785749429.5741448, "n": 2606, "h": "8a1c8ea92278e867", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER:-sentinel} -d ${POSTGRES_DB:-sentinel}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://localhost:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n NODE_ENV: ${NODE_ENV:-development}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n BOARD_ACCESS_LABEL: ${BOARD_ACCESS_LABEL:-Board atelier}\n BOARD_SESSION_TTL_HOURS: ${BOARD_SESSION_TTL_HOURS:-12}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:-}\n LOG_LEVEL: ${LOG_LEVEL:-}\n GLOBAL_API_RATE_LIMIT_MAX: ${GLOBAL_API_RATE_LIMIT_MAX:-}\n GLOBAL_API_RATE_LIMIT_WINDOW_MS: ${GLOBAL_API_RATE_LIMIT_WINDOW_MS:-}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:3000}\n container_name: sentinel_frontend\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n\n caddy:\n image: caddy:2-alpine\n container_name: sentinel_caddy\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost}\n depends_on:\n - frontend\n - backend\n networks:\n - internal\n\nnetworks:\n internal:\n driver: bridge\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "1e2b0612f0ac4f0451e77737af4e0e5ed69eb94d": {"t": 1785749430.6644845, "n": 2434, "h": "2f2645a060f21557", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER:-sentinel} -d ${POSTGRES_DB:-sentinel}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n depends_on:\n postgres:\n condition: service_healthy\n networks:\n - internal\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://localhost:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n NODE_ENV: ${NODE_ENV:-development}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n BOARD_ACCESS_LABEL: ${BOARD_ACCESS_LABEL:-Board atelier}\n BOARD_SESSION_TTL_HOURS: ${BOARD_SESSION_TTL_HOURS:-12}\n BOARD_ACCESS_CODE_HASH: ${BOARD_ACCESS_CODE_HASH:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n TRUST_PROXY: ${TRUST_PROXY:-}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:3000}\n container_name: sentinel_frontend\n depends_on:\n backend:\n condition: service_healthy\n networks:\n - internal\n\n caddy:\n image: caddy:2-alpine\n container_name: sentinel_caddy\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost}\n depends_on:\n - frontend\n - backend\n networks:\n - internal\n\nnetworks:\n internal:\n driver: bridge\n\nvolumes:\n sentinel_data:\n caddy_data:\n caddy_config:\n"}, "67deb0423891e25c41ac96b2c95f0a6779566483": {"t": 1785749431.7378166, "n": 1637, "h": "82298cacc6991000", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U sentinel -d sentinel\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n ports:\n - \"3000:3000\"\n depends_on:\n postgres:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"wget -qO- http://localhost:3000/api/health > /dev/null || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n NODE_ENV: ${NODE_ENV:-development}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:3000}\n container_name: sentinel_frontend\n ports:\n - \"5173:80\"\n depends_on:\n backend:\n condition: service_healthy\n\nvolumes:\n sentinel_data:\n"}, "d264c346abfee320e2f5f77c85c53f66f8d65bd3": {"t": 1785749432.938781, "n": 1581, "h": "d2222b9a298e4fc5", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-sentinel}\n POSTGRES_USER: ${POSTGRES_USER:-sentinel}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sentinel_password}\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n ports:\n - \"5432:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U sentinel -d sentinel\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n ports:\n - \"3000:3000\"\n depends_on:\n postgres:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/api/health || exit 1\"]\n interval: 10s\n timeout: 5s\n retries: 5\n environment:\n PORT: ${PORT:-3000}\n DATABASE_URL: ${DATABASE_URL:-postgres://sentinel:sentinel_password@postgres:5432/sentinel}\n ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}\n ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}\n COOKIE_SECRET: ${COOKIE_SECRET:-change_me_in_production}\n CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:5173}\n JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me_in_production}\n NODE_ENV: ${NODE_ENV:-development}\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n container_name: sentinel_frontend\n ports:\n - \"5173:80\"\n depends_on:\n backend:\n condition: service_healthy\n\nvolumes:\n sentinel_data:\n"}, "b440bd9fbcf969f63b3f4a7b8da93e8139626c98": {"t": 1785749434.1878042, "n": 1210, "h": "97c254aa308a2c5b", "c": "version: '3.9'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: sentinel_postgres\n environment:\n POSTGRES_DB: sentinel\n POSTGRES_USER: sentinel\n POSTGRES_PASSWORD: sentinel_password\n volumes:\n - sentinel_data:/var/lib/postgresql/data\n ports:\n - \"5432:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U sentinel -d sentinel\"]\n interval: 5s\n timeout: 5s\n retries: 10\n\n backend:\n build:\n context: ./backend\n dockerfile: Dockerfile\n container_name: sentinel_backend\n ports:\n - \"3000:3000\"\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n PORT: 3000\n DATABASE_URL: postgres://sentinel:sentinel_password@postgres:5432/sentinel\n ADMIN_USERNAME: admin\n ADMIN_PASSWORD: admin123\n COOKIE_SECRET: change_me_in_production\n CLIENT_ORIGIN: http://localhost:5173\n JWT_SECRET: jwt_secret_change_me_in_production\n NODE_ENV: production\n\n frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile\n container_name: sentinel_frontend\n ports:\n - \"5173:80\"\n depends_on:\n - backend\n\nvolumes:\n sentinel_data:\n"}}, "backend/src/config/__tests__/production.test.ts": {"492826051c9013f6780aabba286e0954d6a2586f": {"t": 1786068280.4774873, "n": 11685, "h": "03749c761400cb32", "c": "import {\n assertProductionConfig,\n parseBooleanEnv,\n parseClientOrigin,\n parseIntegerEnv,\n parsePort,\n parseTrustProxy,\n} from '../production';\nimport { createCsrfProtection } from '../../middlewares/csrfProtection';\n\nconst ORIGINAL_ENV = process.env;\n\nfunction setProductionEnv(overrides: NodeJS.ProcessEnv = {}): void {\n process.env = {\n NODE_ENV: 'production',\n DATABASE_URL: 'postgres://sentinel:strong_database_password@postgres:5432/sentinel',\n ADMIN_USERNAME: 'admin',\n ADMIN_PASSWORD: 'very-long-admin-password',\n COOKIE_SECRET: '12345678901234567890123456789012',\n JWT_SECRET: 'abcdefghijklmnopqrstuvwxyz123456',\n CLIENT_ORIGIN: 'https://sentinel.akiksystems.fr',\n TRUST_PROXY: 'true',\n BOARD_ACCESS_CODE_HASH: '$2b$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ01234',\n BUILD_SHA: '0123456789abcdef0123456789abcdef01234567',\n DEEPSEEK_API_KEY: 'test-deepseek-key',\n SMTP_HOST: 'smtp.example.com',\n SMTP_PORT: '587',\n SMTP_SECURE: 'false',\n SMTP_FROM: 'Sentinel ',\n ADMIN_EMAIL: 'admin@example.com',\n ...overrides,\n };\n}\n\ndescribe('assertProductionConfig', () => {\n afterEach(() => {\n process.env = ORIGINAL_ENV;\n });\n\n it('allows non-production startup', () => {\n process.env = { NODE_ENV: 'development' };\n\n expect(() => assertProductionConfig()).not.toThrow();\n });\n\n it('rejects default production secrets', () => {\n setProductionEnv({ JWT_SECRET: 'jwt_secret_change_me_in_production' });\n\n expect(() => assertProductionConfig()).toThrow('Unsafe production secret values');\n });\n\n it('rejects placeholder secrets even when they are long enough', () => {\n setProductionEnv({ COOKIE_SECRET: 'replace_with_64_hex_chars_replace_with_64_hex_chars' });\n\n expect(() => assertProductionConfig()).toThrow('COOKIE_SECRET');\n });\n\n it('requires distinct cookie and JWT secrets', () => {\n const sharedSecret = 'a-shared-secret-that-is-at-least-32-characters';\n setProductionEnv({ COOKIE_SECRET: sharedSecret, JWT_SECRET: sharedSecret });\n\n expect(() => assertProductionConfig()).toThrow('must be distinct');\n });\n\n it('allows bootstrap admin credentials to be removed after first deployment', () => {\n setProductionEnv({ ADMIN_USERNAME: undefined, ADMIN_PASSWORD: undefined });\n\n expect(() => assertProductionConfig()).not.toThrow();\n });\n\n it('rejects a weak bootstrap password when one is provided', () => {\n setProductionEnv({ ADMIN_PASSWORD: 'admin123' });\n\n expect(() => assertProductionConfig()).toThrow('ADMIN_PASSWORD');\n });\n\n it('rejects a bootstrap password beyond the bcrypt byte boundary', () => {\n setProductionEnv({ ADMIN_PASSWORD: 'é'.repeat(37) });\n\n expect(() => assertProductionConfig()).toThrow('ADMIN_PASSWORD');\n });\n\n it('rejects a numeric-only administrator username', () => {\n setProductionEnv({ ADMIN_USERNAME: '0012' });\n\n expect(() => assertProductionConfig()).toThrow('uniquement numérique');\n });\n\n it('rejects localhost production origin', () => {\n setProductionEnv({ CLIENT_ORIGIN: 'http://localhost:5173' });\n\n expect(() => assertProductionConfig()).toThrow('CLIENT_ORIGIN');\n });\n\n it('rejects missing board access hash', () => {\n setProductionEnv({ BOARD_ACCESS_CODE_HASH: '' });\n\n expect(() => assertProductionConfig()).toThrow('BOARD_ACCESS_CODE_HASH');\n });\n\n it('rejects invalid board access hash', () => {\n setProductionEnv({ BOARD_ACCESS_CODE_HASH: 'replace_with_bcrypt_of_board_code' });\n\n expect(() => assertProductionConfig()).toThrow('BOARD_ACCESS_CODE_HASH');\n });\n\n it('requires an exact deployable Git revision', () => {\n setProductionEnv({ BUILD_SHA: 'main' });\n\n expect(() => assertProductionConfig()).toThrow('BUILD_SHA');\n });\n\n it('rejects a legacy fast SHA-256 board hash', () => {\n setProductionEnv({\n BOARD_ACCESS_CODE_HASH: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6',\n });\n\n expect(() => assertProductionConfig()).toThrow('bcrypt');\n });\n\n it('requires an HTTPS origin without a path', () => {\n setProductionEnv({ CLIENT_ORIGIN: 'http://sentinel.example.com' });\n expect(() => assertProductionConfig()).toThrow('HTTPS');\n\n setProductionEnv({ CLIENT_ORIGIN: 'https://sentinel.example.com/app' });\n expect(() => assertProductionConfig()).toThrow('origin');\n });\n\n it('rejects a trailing slash instead of diverging from the runtime CSRF guard', () => {\n setProductionEnv({ CLIENT_ORIGIN: 'https://sentinel.akiksystems.fr/' });\n\n expect(() => assertProductionConfig()).toThrow('canonical absolute origin');\n expect(() => createCsrfProtection(process.env.CLIENT_ORIGIN!)).toThrow(\n 'canonical absolute origin'\n );\n });\n\n it('rejects a placeholder production origin', () => {\n setProductionEnv({ CLIENT_ORIGIN: 'https://sentinel.example.com' });\n\n expect(() => assertProductionConfig()).toThrow('placeholder hostname');\n });\n\n it('requires a complete PostgreSQL URL with a strong password', () => {\n setProductionEnv({ DATABASE_URL: 'mysql://sentinel:strong_password@postgres/sentinel' });\n expect(() => assertProductionConfig()).toThrow('postgres');\n\n setProductionEnv({\n DATABASE_URL:\n 'postgres://sentinel:replace_with_a_long_random_database_password@postgres/sentinel',\n });\n expect(() => assertProductionConfig()).toThrow('non-placeholder');\n\n setProductionEnv({ DATABASE_URL: 'postgres://sentinel:short@postgres/sentinel' });\n expect(() => assertProductionConfig()).toThrow('strong');\n });\n\n it('requires explicit reverse-proxy trust in production', () => {\n setProductionEnv({ TRUST_PROXY: '' });\n expect(() => assertProductionConfig()).toThrow('TRUST_PROXY');\n\n setProductionEnv({ TRUST_PROXY: 'false' });\n expect(() => assertProductionConfig()).toThrow('must be true');\n });\n\n it('rejects invalid runtime ports', () => {\n setProductionEnv({ PORT: '3000oops' });\n expect(() => assertProductionConfig()).toThrow('PORT');\n\n setProductionEnv({ PORT: '65536' });\n expect(() => assertProductionConfig()).toThrow('PORT');\n });\n\n it('rejects invalid production runtime limits', () => {\n setProductionEnv({ GLOBAL_API_RATE_LIMIT_MAX: '0' });\n expect(() => assertProductionConfig()).toThrow('GLOBAL_API_RATE_LIMIT_MAX');\n\n setProductionEnv({ SUPPORT_API_TIMEOUT_MS: '60000' });\n expect(() => assertProductionConfig()).toThrow('SUPPORT_API_TIMEOUT_MS');\n\n setProductionEnv({ NOTIFICATION_BATCH_SIZE: 'many' });\n expect(() => assertProductionConfig()).toThrow('NOTIFICATION_BATCH_SIZE');\n });\n\n it('requires a coherent SMTP configuration when delivery is enabled', () => {\n setProductionEnv({ SMTP_HOST: 'smtp.example.com', SMTP_FROM: undefined });\n expect(() => assertProductionConfig()).toThrow('SMTP_FROM');\n\n setProductionEnv({\n SMTP_HOST: 'smtp.example.com',\n SMTP_FROM: 'Sentinel ',\n SMTP_USER: 'sentinel',\n SMTP_PASS: undefined,\n });\n expect(() => assertProductionConfig()).toThrow('configured together');\n\n setProductionEnv({\n SMTP_HOST: 'smtp.example.com',\n SMTP_FROM: 'Sentinel ',\n SMTP_PORT: 'invalid',\n });\n expect(() => assertProductionConfig()).toThrow('SMTP_PORT');\n });\n});\n\ndescribe('parseClientOrigin', () => {\n it('accepts canonical HTTPS production and explicit local HTTP test origins', () => {\n expect(parseClientOrigin('https://sentinel.akiksystems.fr', 'production')).toBe(\n 'https://sentinel.akiksystems.fr'\n );\n expect(parseClientOrigin('https://sentinel.akiksystems.fr:8443', 'production')).toBe(\n 'https://sentinel.akiksystems.fr:8443'\n );\n expect(parseClientOrigin('http://localhost:5173', 'test')).toBe('http://localhost:5173');\n expect(parseClientOrigin('http://127.0.0.1:5173', 'development')).toBe('http://127.0.0.1:5173');\n expect(parseClientOrigin('http://[::1]:5173', 'test')).toBe('http://[::1]:5173');\n });\n\n it('uses NODE_ENV=development when the caller omits the explicit environment', () => {\n const previousNodeEnv = process.env.NODE_ENV;\n process.env.NODE_ENV = 'development';\n try {\n expect(parseClientOrigin('http://localhost:5173')).toBe('http://localhost:5173');\n } finally {\n process.env.NODE_ENV = previousNodeEnv;\n }\n });\n\n it.each([\n undefined,\n '',\n ' https://sentinel.akiksystems.fr',\n 'https://sentinel.akiksystems.fr/',\n 'https://sentinel.akiksystems.fr:443',\n 'https://user@sentinel.akiksystems.fr',\n 'https://sentinel.akiksystems.fr/path',\n 'https://sentinel.akiksystems.fr?query=yes',\n 'https://sentinel.akiksystems.fr#fragment',\n 'https://*.akiksystems.fr',\n 'ftp://sentinel.akiksystems.fr',\n ])('rejects a missing, malformed or non-canonical origin: %p', (origin) => {\n expect(() => parseClientOrigin(origin, 'production')).toThrow('CLIENT_ORIGIN');\n });\n\n it.each([\n ['production', 'http://localhost:5173'],\n ['production', 'http://sentinel.akiksystems.fr'],\n ['development', 'http://sentinel.akiksystems.fr'],\n ['test', 'http://untrusted.localhost:5173'],\n ['staging', 'http://localhost:5173'],\n ])('rejects HTTP outside an explicitly local development/test origin', (nodeEnv, origin) => {\n expect(() => parseClientOrigin(origin, nodeEnv)).toThrow('CLIENT_ORIGIN');\n });\n\n it('uses the same canonical contract in production preflight and runtime CSRF', () => {\n for (const origin of [\n 'https://sentinel.akiksystems.fr/',\n 'https://sentinel.akiksystems.fr/path',\n 'https://sentinel.akiksystems.fr?query=yes',\n 'https://sentinel.akiksystems.fr#fragment',\n 'https://user@sentinel.akiksystems.fr',\n ]) {\n setProductionEnv({ CLIENT_ORIGIN: origin });\n\n expect(() => assertProductionConfig()).toThrow('canonical absolute origin');\n expect(() => createCsrfProtection(origin)).toThrow('canonical absolute origin');\n }\n });\n});\n\ndescribe('parseTrustProxy', () => {\n it.each([\n [undefined, false],\n ['', false],\n ['false', false],\n ['0', false],\n ['true', 1],\n ['1', 1],\n [' TRUE ', 1],\n ] as const)('normalise %p vers %p', (value, expected) => {\n expect(parseTrustProxy(value)).toBe(expected);\n });\n\n it('refuse les valeurs ambiguës', () => {\n expect(() => parseTrustProxy('loopback')).toThrow('TRUST_PROXY');\n });\n});\n\ndescribe('parsePort', () => {\n it.each([\n [undefined, 3000],\n ['', 3000],\n ['1', 1],\n [' 587 ', 587],\n ['65535', 65_535],\n ] as const)('normalise %p vers %p', (value, expected) => {\n expect(parsePort(value, 'PORT', 3000)).toBe(expected);\n });\n\n it.each(['0', '-1', '1.5', '3000oops', '65536'])('refuse %p', (value) => {\n expect(() => parsePort(value, 'PORT', 3000)).toThrow('PORT');\n });\n});\n\ndescribe('parseBooleanEnv', () => {\n it.each([\n [undefined, false],\n ['', false],\n ['true', true],\n ['1', true],\n ['false', false],\n ['0', false],\n ] as const)('normalise %p vers %p', (value, expected) => {\n expect(parseBooleanEnv(value, 'SMTP_SECURE', false)).toBe(expected);\n });\n\n it('refuse une valeur ambiguë', () => {\n expect(() => parseBooleanEnv('yes', 'SMTP_SECURE', false)).toThrow('SMTP_SECURE');\n });\n});\n\ndescribe('parseIntegerEnv', () => {\n it.each([\n [undefined, 10],\n ['', 10],\n ['1', 1],\n [' 25 ', 25],\n ['100', 100],\n ] as const)('normalise %p vers %p', (value, expected) => {\n expect(parseIntegerEnv(value, 'LIMIT', 10, 1, 100)).toBe(expected);\n });\n\n it.each(['0', '-1', '1.5', '10items', '101'])('refuse %p', (value) => {\n expect(() => parseIntegerEnv(value, 'LIMIT', 10, 1, 100)).toThrow('LIMIT');\n });\n});\n"}}}}