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 = ` Politique de Confidentialité — XAMLÉ Studio

Politique de Confidentialité

Dernière mise à jour : 7 mars 2026

1. Qui sommes-nous ?

XAMLÉ Studio est une plateforme d'éducation entrepreneuriale accessible via WhatsApp, opérée par xamlé.studio. Contact : contact@xamle.studio

2. Données collectées

Lors de votre inscription et utilisation du service, nous collectons :

3. Utilisation des données

Vos données sont utilisées pour :

4. Partage des données

Nous ne vendons jamais vos données. Elles peuvent être partagées uniquement avec :

5. Conservation des données

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 à contact@xamle.studio.

6. Vos droits

Conformément au RGPD et aux lois applicables, vous disposez du droit de :

Pour exercer ces droits, contactez-nous à : contact@xamle.studio

7. Sécurité

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.

8. Modifications

Cette politique peut être mise à jour. En cas de modification majeure, vous serez informé via WhatsApp.

9. Contact

Pour toute question relative à cette politique : contact@xamle.studio

`; 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();