CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf | import dns from 'node:dns'; | |
| dns.setDefaultResultOrder('ipv4first'); | |
| import Fastify from 'fastify'; | |
| import cors from '@fastify/cors'; | |
| import { whatsappRoutes } from './routes/whatsapp'; | |
| import { adminRoutes } from './routes/admin'; | |
| import { aiRoutes } from './routes/ai'; | |
| import { paymentRoutes, stripeWebhookRoute } from './routes/payments'; | |
| import { internalRoutes } from './routes/internal'; | |
| import { studentRoutes } from './routes/student'; | |
| // ── Fail-fast: vérifier les secrets critiques au démarrage ───────────────────── | |
| // Only WHATSAPP_VERIFY_TOKEN is strictly needed at startup (for Meta webhook validation). | |
| // All other secrets are validated lazily (guarded routes return 503 if missing). | |
| const REQUIRED_ENV = ['WHATSAPP_VERIFY_TOKEN']; | |
| const WARN_ENV = ['ADMIN_API_KEY', 'WHATSAPP_APP_SECRET']; | |
| for (const key of REQUIRED_ENV) { | |
| if (!process.env[key]) { | |
| console.error(`[STARTUP] ❌ Missing required environment variable: ${key}`); | |
| process.exit(1); | |
| } | |
| } | |
| for (const key of WARN_ENV) { | |
| if (!process.env[key]) { | |
| console.warn(`[STARTUP] ⚠️ ${key} not set — related features will be degraded`); | |
| } | |
| } | |
| const server = Fastify({ | |
| logger: true, | |
| ignoreTrailingSlash: true | |
| }); | |
| // ── CORS ─────────────────────────────────────────────────────────────────────── | |
| server.register(cors); | |
| // ── Rate Limiting (lazy import — package installed at runtime on HF) ─────────── | |
| async function setupRateLimit() { | |
| try { | |
| const rateLimit = await import('@fastify/rate-limit'); | |
| await server.register(rateLimit.default, { | |
| global: true, | |
| max: 300, | |
| timeWindow: '1 minute', | |
| }); | |
| console.log('[RATE-LIMIT] Rate limiting enabled (300 req/min global)'); | |
| } catch { | |
| console.warn('[RATE-LIMIT] @fastify/rate-limit not available — skipping'); | |
| } | |
| } | |
| // ── Public Routes (no auth) ──────────────────────────────────────────────────── | |
| server.register(whatsappRoutes, { prefix: '/v1/whatsapp' }); | |
| server.register(studentRoutes, { prefix: '/v1/student' }); | |
| // ── Stripe Webhook (public — Stripe can't send API Key, verified by signature) ─ | |
| server.register(stripeWebhookRoute, { prefix: '/v1/payments' }); | |
| // ── Private Routes (require ADMIN_API_KEY) ───────────────────────────────────── | |
| server.register(async function guardedRoutes(scope) { | |
| scope.addHook('onRequest', async (request, reply) => { | |
| const apiKey = process.env.ADMIN_API_KEY; | |
| if (!apiKey) { | |
| request.log.error('ADMIN_API_KEY is not configured!'); | |
| return reply.code(503).send({ error: 'Service misconfigured' }); | |
| } | |
| const authHeader = request.headers['authorization']; | |
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | |
| return reply.code(401).send({ error: 'Unauthorized', message: 'Missing Authorization header' }); | |
| } | |
| const token = authHeader.slice(7); | |
| if (token !== apiKey) { | |
| return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid API key' }); | |
| } | |
| }); | |
| scope.register(adminRoutes, { prefix: '/v1/admin' }); | |
| scope.register(aiRoutes, { prefix: '/v1/ai' }); | |
| scope.register(paymentRoutes, { prefix: '/v1/payments' }); | |
| scope.register(internalRoutes); | |
| }); | |
| // ── Health Routes (public) ───────────────────────────────────────────────────── | |
| server.get('/', async (_req, reply) => { | |
| return reply.code(200).type('application/json').send({ ok: true }); | |
| }); | |
| server.get('/debug/net', async (_req, reply) => { | |
| try { | |
| const res = await fetch('https://www.google.com', { method: 'GET' }); | |
| return reply.send({ ok: true, status: res.status }); | |
| } catch (e: unknown) { | |
| return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) }); | |
| } | |
| }); | |
| server.get('/debug/graph', async (_req, reply) => { | |
| try { | |
| const res = await fetch('https://graph.facebook.com', { method: 'GET' }); | |
| return reply.send({ ok: true, status: res.status }); | |
| } catch (e: unknown) { | |
| return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) }); | |
| } | |
| }); | |
| server.get('/health', async () => { | |
| return { status: 'ok', timestamp: new Date().toISOString() }; | |
| }); | |
| // ── Privacy Policy (required by Meta for app publication) ────────────────────── | |
| server.get('/v1/privacy', async (_req, reply) => { | |
| const html = `<!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Politique de Confidentialité — XAMLÉ Studio</title> | |
| <style> | |
| body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; color: #1e293b; line-height: 1.7; } | |
| h1 { font-size: 2rem; margin-bottom: 4px; } | |
| h2 { font-size: 1.2rem; margin-top: 2rem; color: #0f172a; } | |
| p, li { color: #334155; } | |
| a { color: #059669; } | |
| .updated { color: #94a3b8; font-size: 0.9rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Politique de Confidentialité</h1> | |
| <p class="updated">Dernière mise à jour : 7 mars 2026</p> | |
| <h2>1. Qui sommes-nous ?</h2> | |
| <p>XAMLÉ Studio est une plateforme d'éducation entrepreneuriale accessible via WhatsApp, opérée par xamlé.studio. Contact : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p> | |
| <h2>2. Données collectées</h2> | |
| <p>Lors de votre inscription et utilisation du service, nous collectons :</p> | |
| <ul> | |
| <li>Votre numéro de téléphone WhatsApp</li> | |
| <li>La langue choisie (Français ou Wolof)</li> | |
| <li>Votre secteur d'activité / projet professionnel (fourni volontairement)</li> | |
| <li>Vos réponses aux exercices et contenus pédagogiques</li> | |
| <li>Les métadonnées de vos messages (horodatage, type de message)</li> | |
| </ul> | |
| <h2>3. Utilisation des données</h2> | |
| <p>Vos données sont utilisées pour :</p> | |
| <ul> | |
| <li>Vous envoyer des leçons quotidiennes personnalisées via WhatsApp</li> | |
| <li>Personnaliser le contenu pédagogique à votre secteur d'activité</li> | |
| <li>Générer des documents AI (One-Pager PDF, Pitch Deck) basés sur votre parcours</li> | |
| <li>Améliorer la qualité de nos formations</li> | |
| <li>Traiter vos paiements pour les formations premium</li> | |
| </ul> | |
| <h2>4. Partage des données</h2> | |
| <p>Nous ne vendons jamais vos données. Elles peuvent être partagées uniquement avec :</p> | |
| <ul> | |
| <li><strong>Meta / WhatsApp</strong> — pour l'acheminement des messages</li> | |
| <li><strong>OpenAI</strong> — pour la génération et personnalisation du contenu pédagogique</li> | |
| <li><strong>Stripe</strong> — pour le traitement sécurisé des paiements</li> | |
| <li><strong>Cloudflare</strong> — pour le stockage des documents générés</li> | |
| </ul> | |
| <h2>5. Conservation des données</h2> | |
| <p>Vos données sont conservées pendant toute la durée de votre inscription active. Vous pouvez demander la suppression de vos données à tout moment en envoyant un e-mail à <a href="mailto:contact@xamle.studio">contact@xamle.studio</a>.</p> | |
| <h2>6. Vos droits</h2> | |
| <p>Conformément au RGPD et aux lois applicables, vous disposez du droit de :</p> | |
| <ul> | |
| <li>Accéder à vos données personnelles</li> | |
| <li>Corriger les données inexactes</li> | |
| <li>Demander la suppression de vos données</li> | |
| <li>Vous opposer au traitement de vos données</li> | |
| </ul> | |
| <p>Pour exercer ces droits, contactez-nous à : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p> | |
| <h2>7. Sécurité</h2> | |
| <p>Vos données sont protégées par chiffrement (TLS en transit, AES au repos). L'accès aux données est strictement limité aux systèmes nécessaires au fonctionnement du service.</p> | |
| <h2>8. Modifications</h2> | |
| <p>Cette politique peut être mise à jour. En cas de modification majeure, vous serez informé via WhatsApp.</p> | |
| <h2>9. Contact</h2> | |
| <p>Pour toute question relative à cette politique : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p> | |
| </body> | |
| </html>`; | |
| return reply.code(200).type('text/html').send(html); | |
| }); | |
| // ── Start Server ─────────────────────────────────────────────────────────────── | |
| const start = async () => { | |
| try { | |
| await setupRateLimit(); | |
| const port = parseInt(process.env.PORT || '8080'); | |
| const isGateway = process.env.IS_GATEWAY === 'true' || process.env.HF_SPACE_ID !== undefined; | |
| console.log(`[STARTUP] Mode: ${isGateway ? 'GATEWAY (Forwarding Only)' : 'DIRECT (Processing)'}`); | |
| console.log(`[STARTUP] Forwarding to: ${process.env.RAILWAY_INTERNAL_URL || 'NONE'}`); | |
| await server.listen({ port, host: '0.0.0.0' }); | |
| console.log(`Server listening on http://0.0.0.0:${port}`); | |
| } catch (err) { | |
| console.error(err); | |
| process.exit(1); | |
| } | |
| }; | |
| start(); | |