# Déploiement sur o2switch (cPanel)

Compte cPanel : `mixq8738`. Domaine : `advmfrkb.elevaseo.app`.
Dépôt : `github.com/bastienallain/advmfr-knowledge`.

## Deux obstacles, et leur cause commune

Le serveur porte une **glibc antérieure à 2.29**. C'est le fait dont tout découle.

### Ce qu'on voit d'abord

```
RangeError: WebAssembly.instantiate(): Out of memory:
Cannot allocate Wasm memory for new instance
```

Le message parle de mémoire, mais la mémoire n'a rien à voir : le compte dispose
de 4 Gio et le build en consomme 2,1 en local. Ce qui bloque est la ligne d'à
côté dans les limites du processus :

```
Max address space    4294967296   →  4 Gio
```

Node active par défaut des vérifications de bornes WebAssembly fondées sur un
*gestionnaire de trap*. Cette optimisation oblige V8 à réserver une **cage
d'espace d'adressage virtuel de plusieurs gigaoctets par mémoire Wasm**,
indépendamment de ce qui est réellement utilisé. Sous le `ulimit -v` que
CloudLinux applique, cette réservation ne rentre pas, et n'importe quel module
Wasm échoue, fût-il minuscule.

CloudLinux documente ce cas et Node fournit le drapeau correspondant :
**`--disable-wasm-trap-handler`**. Les bornes sont alors vérifiées explicitement,
sans cage, au prix d'une performance Wasm moindre.

### Ce que cela révèle ensuite

Le drapeau posé, le build va plus loin et affiche enfin la cause de fond :

```
⚠ Attempted to load @next/swc-linux-x64-gnu, but an error occurred:
  /lib64/libm.so.6: version `GLIBC_2.29' not found
  Downloading swc package @next/swc-wasm-nodejs...
```

Le binaire natif de Next **est installé mais ne peut pas se charger** : il exige
une glibc plus récente que celle du serveur. Next se rabat donc sur son
compilateur en WebAssembly, ce qui explique pourquoi du Wasm apparaissait là où
on ne l'attendait pas.

Et Turbopack, lui, n'a pas de version WebAssembly :

```
Error: Turbopack is not supported on this platform (linux/x64)
because native bindings are not available.
To build on this platform, use Webpack instead: next build --webpack
```

D'où le second réglage : **`--webpack`**. Next 16 utilise Turbopack par défaut ;
sur cet hébergement il faut lui demander l'ancien compilateur, seul compatible
avec les liaisons WebAssembly.

Ces deux réglages sont devenus ceux du script `build` par défaut : un projet dont
le build ne passe que sur la machine du développeur n'est pas un projet
déployable. `build:turbopack` reste sous la main pour les itérations locales.

<!-- -->

> **À savoir** : le build passera par le compilateur WebAssembly, sensiblement
> plus lent que le natif. C'est une conséquence de la glibc du serveur, pas un
> réglage à ajuster. Si ce build finit par buter sur la mémoire réelle, le repli
> décrit en fin de document, build en local, devient la voie à suivre.

## Configuration de l'application Node

**Setup Node.js App** → **Create Application**

| Champ | Valeur |
|---|---|
| Node.js version | **24** |
| Application mode | Production |
| Racine de l'application | `advmfr-knowledge/advmfr-knowledge` |
| URL de l'application | `advmfrkb.elevaseo.app` |
| Fichier de démarrage | `server.js` |

**Variables d'environnement**, dans la section *Environment variables* :

| Nom | Valeur | Rôle |
|---|---|---|
| `NODE_OPTIONS` | `--disable-wasm-trap-handler` | contourne le plafond d'adressage, voir plus haut |
| `DATABASE_URL` | `mysql://user:mdp@localhost:3306/base` | base MySQL créée dans cPanel |
| `BETTER_AUTH_SECRET` | 32 caractères au moins | `npx @better-auth/cli secret` |
| `BETTER_AUTH_URL` | `https://advmfrkb.elevaseo.app` | construit les liens envoyés par mail |
| `RESEND_API_KEY` | clé API Resend | envoi des liens |
| `MAGIC_LINK_FROM` | `ADVMFR Knowledge <noreply@advmfr.net>` | expéditeur, sur un domaine **vérifié chez Resend** |

`NODE_OPTIONS` est nécessaire **au démarrage autant qu'au build** : l'application
charge les mêmes primitives Edge Runtime à l'exécution. Sans elle, le build
passera et l'application refusera de démarrer.

`DATABASE_URL` et les deux `BETTER_AUTH_*` sont exigées dès l'import de la
configuration : sans base ni secret, l'application ne peut rien servir. Les
variables d'envoi, elles, ne sont validées **qu'au premier message**: un build
n'envoie aucun mail et n'a pas à connaître la clé.

Cette distinction n'est pas cosmétique : tant qu'elles étaient exigées à
l'import, `next build` échouait pendant la collecte des routes, une étape qui ne
touche pas au courrier.

Le contrôle n'est pas perdu : une configuration incomplète fait échouer le
premier envoi avec un message nommant ce qui manque, tracé dans le log Passenger.

<!-- -->

> **Le domaine de `MAGIC_LINK_FROM` doit être vérifié dans Resend**, avec ses
> enregistrements DKIM et SPF publiés en DNS. Sans cela Resend n'accepte qu'un
> seul destinataire, le titulaire du compte. Les autres verront « votre lien est
> parti » et ne recevront jamais rien : le refus est tracé dans le log, nulle part
> ailleurs, parce que l'écran doit rester identique pour ne pas révéler qui a
> accès.

> **La racine doit rester hors du dossier du domaine.** o2switch le précise : si
> les deux coïncident, arrêter l'application expose le code source, servi comme
> de simples fichiers.

`server.js` est fourni par le dépôt. Il démarre Next par son API programmatique
plutôt que par `output: 'standalone'`, dont le bundle attend d'être lancé depuis
son propre dossier avec les assets recopiés à côté, alors que Passenger fixe le
répertoire courant à la racine de l'application.

## Build

En SSH, jamais dans le terminal du cPanel, dont la limite de mémoire est plus
basse.

```bash
source /home/mixq8738/nodevenv/advmfr-knowledge/advmfr-knowledge/24/bin/activate \
  && cd /home/mixq8738/advmfr-knowledge/advmfr-knowledge

git clone --branch main https://github.com/bastienallain/advmfr-knowledge.git .
npm install --include=dev
npm run build
```

`--include=dev` n'est pas décoratif. Le mode *Production* de l'application pose
`NODE_ENV=production`, et npm saute alors les dépendances de développement. Or
`@tailwindcss/postcss` et `typescript` en font partie : sans elles le build
échoue sur `Cannot find module '@tailwindcss/postcss'`, puis sur une cascade de
`Module not found: Can't resolve '@/lib/...'`, Next ne lisant plus les chemins
de `tsconfig.json` faute de TypeScript. Deux symptômes très différents pour une
seule cause.

Le script `build` réunit les deux réglages imposés par le serveur : le drapeau
Node et le compilateur webpack. Il est le build par défaut du projet, y compris
en local, pour que ce qui est vérifié sur une machine soit ce qui tourne sur
l'autre. `build:turbopack` reste disponible quand la vitesse prime et que le
binaire natif est chargeable.

La commande `source` est celle qu'affiche cPanel après enregistrement. Elle change
si la racine de l'application change : la recopier depuis l'interface plutôt que
depuis ce document.

Aucune variable d'environnement applicative n'est nécessaire : la protection
d'accès vit dans cPanel, pas dans l'application.

Le serveur suit `main`, la branche de production. Les mises à jour se font par
`git pull` après fusion de `dev` dans `main`.

Un build réussi se termine par le tableau des routes et sa légende
`○ (Static) ● (SSG) ƒ (Dynamic)`.

### Si `git pull` refuse de fusionner

```
erreur : Les fichiers suivants non suivis de la copie de travail seraient
effacés par la fusion : server.js
```

Un fichier créé à la main sur le serveur porte le même nom qu'un fichier
désormais fourni par le dépôt. Le déplacer suffit :

```bash
mv server.js server.js.local && git pull
```

Le symptôme trompeur est qu'une commande lancée juste après échouera sur
`Missing script` ou sur du code périmé : la fusion ayant été abandonnée, la copie
de travail n'a pas bougé.

## Démarrage

Après un build réussi : **Redémarrer** l'application dans cPanel. Passenger lance
`server.js`, qui écoute sur le port fourni dans `PORT`.

En cas de 503 muette, la cause est dans le fichier de log Passenger renseigné à la
création de l'application, `server.js` y écrit son erreur de démarrage. Le premier
suspect est l'absence de `NODE_OPTIONS` : le symptôme serait alors la même erreur
Wasm qu'au build.

## Authentification

Après le build, une fois les variables en place :

```bash
npm run auth:migrate                                  # crée les tables
npm run invite add prenom@exemple.fr "Prénom Nom"     # ouvre l'accès
npm run invite list                                   # qui a accès
npm run invite remove prenom@exemple.fr               # ferme l'accès
```

L'allowlist **est** la table `user` : inviter quelqu'un crée sa ligne, le révoquer
la supprime. Il n'y a pas de seconde liste qui pourrait la contredire.

Une adresse non invitée qui demande un lien reçoit exactement la même réponse
qu'une adresse invitée, et aucun mail ne part. C'est ce qui empêche de découvrir
qui a accès en essayant des adresses.

`invite` est du JavaScript nu, sans TypeScript ni alias : il tourne sous le Node
de l'hébergement et fonctionne même si l'application ne compile pas.

<!-- -->

> **Retirer la Confidentialité du répertoire** une fois l'authentification
> vérifiée. Les deux protections superposées demanderaient deux mots de passe
> pour une seule porte.

### Limitation de débit

Mesurée en place : trois demandes de lien par quart d'heure et par adresse IP,
puis `429` avec un en-tête `X-Retry-After`.

| Chemin | Fenêtre | Maximum |
|---|---|---|
| *(tout le reste)* | 60 s | 60 |
| `/sign-in/magic-link` | 900 s | 3 |
| `/magic-link/verify` | 900 s | 10 |
| `/get-session` | 60 s | 240 |

Deux réglages en dépendent, et l'un comme l'autre est invisible tant qu'il
manque :

**Les compteurs vivent en base**, dans une table `rateLimit` que `auth:migrate`
crée. Passenger peut faire tourner plusieurs processus et les redémarre à sa
guise : des compteurs en mémoire seraient multipliés par le nombre de processus
et remis à zéro à chaque redémarrage, une limite qui ne limite rien au moment
où elle compte. **Relancer `npm run auth:migrate`** après cette mise à jour.

**L'adresse du client est lue dans `cf-connecting-ip`**, l'en-tête que pose
Cloudflare. Sans ce réglage, Better Auth lit `x-forwarded-for`, qu'un client peut
falsifier : la limite serait contournable en une requête.

Corollaire à ne pas négliger : **l'origine ne doit être joignable qu'à travers
Cloudflare.** Une requête qui l'atteint directement n'a pas cet en-tête ; Better
Auth bascule alors sur un compteur unique partagé par tout le trafic non
identifié, ce qui le bride sévèrement, mais elle échappe du même coup à tout ce
que Cloudflare filtre en amont.

## Vérifier l'accès

```bash
curl -sI https://advmfrkb.elevaseo.app/docs | head -1          # 307 vers /login
curl -sI https://advmfrkb.elevaseo.app/llms-full.txt | head -1 # 401
curl -sI "https://advmfrkb.elevaseo.app/api/search?query=a" | head -1  # 401
```

Les pages redirigent vers l'écran de connexion ; les routes qui servent des
agents refusent avec un 401, une redirection leur renverrait du HTML.

**Le certificat TLS doit être valide.** Le cookie de session porte le préfixe
`__Secure-`, qu'un navigateur refuse hors HTTPS de confiance : tant qu'AutoSSL n'a
pas émis le certificat du sous-domaine, personne ne peut se connecter, même avec
des identifiants corrects.

## Le repli, si la voie applicative se dérobe

Le contenu de cette base est du MDX compilé au build ; rien n'exige un serveur, à
deux détails près :

| Ce qui disparaît | Remplacement |
|---|---|
| La recherche côté serveur | `staticGET()` exporte l'index, `staticClient()` le consomme dans le navigateur. Contrat vérifié dans `fumadocs-core`. |
| Le `proxy.ts` | La redirection vers la langue du navigateur. `/` sert l'anglais, `/th` le thaï, les deux existent comme fichiers. |

Le déploiement devient alors : build en local, envoi du dossier `out/` dans le
dossier du domaine. Aucun processus sur le serveur, rien à redémarrer, aucune
limite à contourner.

## Note sur les images OG

La route de génération d'images OG a été supprimée en cours de diagnostic. Elle
n'était **pas** la cause du problème, la vraie cause est décrite plus haut, mais
elle n'avait aucun usage : un site protégé par mot de passe n'a pas d'aperçu
social, aucun réseau ne pouvant charger une image qu'il n'a pas le droit de
récupérer. Sa suppression allège le build de dix-sept rendus SVG inutiles.
