> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/fr/development/adding-a-new-tool.md).

# Ajouter un nouvel outil

Les « Advanced Tools » sur la page d'accueil — MAC Lookup, Whois, DNS Resolver, Censorship Check, et le reste — suivent tous le même schéma de câblage. Cette page explique comment en ajouter un nouveau de bout en bout.

## L'exemple

Nous allons ajouter un hypothétique **Vérification de certificat**: saisissez un domaine, récupérez ses détails de certificat TLS depuis une API amont.

| Partie              | Valeur                                             |
| ------------------- | -------------------------------------------------- |
| Slug                | `certcheck`                                        |
| Composant           | `frontend/components/advanced-tools/CertCheck.vue` |
| Gestionnaire d'API  | `api/cert-check.js`                                |
| Route               | `GET /api/certcheck?domain=…`                      |
| Espace de noms i18n | `certcheck.*`                                      |

Il est directement calqué sur la **Recherche MAC** outil (`macchecker` → `MacChecker.vue` → `api/mac-checker.js`), qui est l'exemple complet le plus petit du dépôt. Ouvrez ces trois fichiers en même temps que cette page.

{% hint style="info" %}
**Tous les outils n'ont pas besoin d'un backend.** Browser Info et la Security Checklist s'exécutent entièrement dans le navigateur. Si le vôtre aussi, sautez les étapes 1 à 3 et allez directement au composant.
{% endhint %}

## Nommage

* **Slug** — en minuscules, sans séparateurs : `macchecker`, `dnsresolver`, `censorshipcheck`. C'est l'URL à `/tools/<slug>` et la requête du tiroir `?tool=<slug>`, donc il est en pratique permanent une fois déployé.
* **Composant** — PascalCase `.vue` dans `frontend/components/advanced-tools/`.
* **Fichier gestionnaire** — kebab-case `.js` dans `api/`.
* **Chemin de la route** — correspond au slug pour les anciens outils (`/api/macchecker`), kebab-case pour les plus récents (`/api/ooni-blocking`, `/api/service-status`). Les deux conviennent ; choisissez-en une et utilisez-la de façon cohérente.

***

{% stepper %}
{% step %}

### Écrivez le gestionnaire d'API

Un fichier par route sous `api/`, en commençant par un commentaire d'en-tête qui indique la route et son objectif. Une seule exportation par défaut, `fetchUpstream` pour l'appel en amont, le logger partagé pour les échecs.

{% code title="api/cert-check.js" %}

```js
// /api/certcheck — détails du certificat TLS pour un domaine, récupérés depuis l'API
// de certificats amont. Alimente l'outil frontend CertCheck.

import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';

const CERT_API_URL = 'https://example-cert-api.test/v1/cert';

export default async (req, res) => {
    if (req.method !== 'GET') {
        return res.status(405).json({ error: 'Méthode non autorisée' });
    }

    // La présence, la forme et la mise en minuscules sont garanties par requireValidDomain.
    const domain = req.query.domain;

    const token = process.env.CERT_API_KEY || '';
    if (!token) {
        return res.status(500).json({ error: 'Clé API manquante' });
    }

    try {
        const upstream = await fetchUpstream(`${CERT_API_URL}?host=${domain}&key=${token}`);
        if (!upstream.ok) {
            throw new Error(`L'API de certificats a répondu avec le statut ${upstream.status}`);
        }
        res.json(await upstream.json());
    } catch (error) {
        logger.error({ err: error, domain }, 'échec du gestionnaire cert-check');
        res.status(500).json({ error: error.message });
    }
};
```

{% endcode %}

Quatre choses qui ne sont pas négociables :

* **`fetchUpstream`, jamais un simple `fetch()`.** Il emporte le délai d'attente de 8 secondes et le User-Agent du projet `User-Agent`.
* **Le logger partagé, jamais `console.*`.** Objet de contexte d'abord, message court ensuite.
* **Des formes d'erreur concises.** `400` sur une entrée incorrecte, `500` avec `{ error: … }` en cas d'échec.
* **Aucun contrôle de referer ou de paramètre dans le gestionnaire.** Le middleware s'en est déjà chargé — voir l'étape suivante.

Le `req.method !== 'GET'` test est défensif : la route ci-dessous restreint déjà la méthode. Il reste là parce que le test de fumée l'évalue directement.
{% endstep %}

{% step %}

### Réutilisez un garde — ou ajoutez-en un

La validation des paramètres vit dans `common/guards.js`, jamais dans les gestionnaires. Notre outil prend `?domain=`, et ce garde existe déjà :

```js
requireValidDomain()   // rejette les domaines manquants ou mal formés, met en minuscules sur place
```

La mise en minuscules sur place est importante : le cache de bord s'appuie sur l'URL, donc une requête en majuscules/minuscules mélangées ne doit pas devenir une entrée de cache distincte.

L'ensemble disponible aujourd'hui :

| Garde                      | Valide                                                    |
| -------------------------- | --------------------------------------------------------- |
| `requireReferer`           | Global sur `/api/*` — domaines autorisés + localhost      |
| `requireValidIP()`         | `?ip=`                                                    |
| `requireValidDomain()`     | `?domain=` (met aussi en minuscules)                      |
| `requireValidPrefix()`     | `?prefix=` (CIDR)                                         |
| `requireValidASN()`        | `?asn=` (supprime `AS`, réécrit en numérique)             |
| `requireValidProviderId()` | `?id=` par rapport à la liste des slugs de service-status |
| `requireValidReportId()`   | `/api/report/:id` paramètre de route                      |

{% hint style="warning" %}
**Une nouvelle forme de paramètre signifie un nouveau garde.** Ajoutez-le comme une fabrique exportée dans `common/guards.js`, attachez-le dans `backend-server.js`, et couvrez-le dans `tests/guards.test.js`. N'implémentez pas le contrôle en dur dans votre gestionnaire — c'est précisément la dérive que la couche de garde existe pour empêcher.
{% endhint %}
{% endstep %}

{% step %}

### Branchez la route dans `backend-server.js`

Chaque route de l'application est déclarée dans ce seul fichier. Importez le gestionnaire en haut, à côté des autres :

{% code title="backend-server.js" %}

```js
import certCheckHandler from './api/cert-check.js';
```

{% endcode %}

Déclarez ensuite la route. L'ordre des middlewares est : garde d'abord, cache ensuite, puis gestionnaire :

{% code title="backend-server.js" %}

```js
app.get('/api/certcheck', requireValidDomain(), cacheable(ONE_DAY_CACHE), certCheckHandler);
```

{% endcode %}

Choisissez le TTL en fonction de la vitesse à laquelle les données amont changent réellement. Le fichier définit déjà les constantes — `FIVE_MIN_CACHE`, `ONE_HOUR_CACHE`, `ONE_DAY_CACHE`, `SEVEN_DAYS_CACHE`, `THIRTY_DAYS_CACHE`, `ONE_YEAR_CACHE` — toutes écrites comme des expressions multipliées plutôt que comme des secondes brutes.

Si les données sont propres à un utilisateur, authentifiées, ou changent à chaque requête, **omettez `cacheable()` entièrement**. Tout ce qui est sous `/api/*` par défaut `Cache-Control: no-store`, donc l'omettre est le choix sûr.

Une nouvelle variable d'environnement ? Ajoutez-la à `.env.example` avec un commentaire, et documentez-la dans [Variables d'environnement](/developer/fr/reference/environment-variables.md) et [Clés API facultatives](/developer/fr/configuration/optional-api-keys.md).
{% endstep %}

{% step %}

### Construisez le composant

Créer `frontend/components/advanced-tools/CertCheck.vue`. C'est un `<script setup>` composant normal — le tiroir et la page autonome le montent tous deux tels quels, donc il n'a besoin ni d'une enveloppe, ni d'une barre de titre, ni d'être conscient de la route.

Copiez les modèles canoniques plutôt que d'en inventer de nouveaux. Depuis `MacChecker.vue`:

{% code title="frontend/components/advanced-tools/CertCheck.vue" %}

```vue
<template>
    <div class="cert-check-section my-4 space-y-4">
        <p class="text-sm text-muted-foreground leading-relaxed">{{ t('certcheck.Note') }}</p>

        <div class="space-y-2">
            <Label for="queryDomain">{{ t('certcheck.Note2') }}</Label>
            <div class="flex items-center gap-2">
                <Input type="text" id="queryDomain" name="queryDomain"
                    autocomplete="off" autocorrect="off" autocapitalize="off"
                    spellcheck="false" data-1p-ignore data-lpignore="true"
                    :disabled="status === 'running'"
                    :placeholder="t('certcheck.Placeholder')"
                    v-model="queryDomain" @keyup.enter="onSubmit" />
                <Button variant="action" :disabled="status === 'running' || !queryDomain"
                    @click="onSubmit" class="cursor-pointer">
                    <Spinner v-if="status === 'running'" />
                    <Search v-else class="size-4 shrink-0" />
                </Button>
            </div>
            <p v-if="errorMsg" class="text-sm text-destructive">{{ errorMsg }}</p>
        </div>

        <!-- Zone des résultats -->
        <Card v-if="result.subject">…</Card>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { Search } from '@lucide/vue';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { Label } from '@/components/ui/label';

const { t } = useI18n();

const queryDomain = ref('');
const status = ref('idle');
const result = ref({});
const errorMsg = ref('');

const onSubmit = () => {
    trackEvent('Section', 'StartClick', 'CertCheck');
    errorMsg.value = '';
    result.value = {};
    if (queryDomain.value) fetchCert(queryDomain.value);
};

const fetchCert = async (domain) => {
    status.value = 'running';
    try {
        const response = await fetch(`/api/certcheck?domain=${domain}`);
        if (!response.ok) throw new Error('La réponse réseau n'était pas correcte');
        result.value = await response.json();
    } catch (error) {
        console.error('Erreur lors de la récupération du certificat :', error);
        errorMsg.value = t('certcheck.fetchError');
    } finally {
        status.value = 'idle';
    }
};
</script>
```

{% endcode %}

Points qui méritent d'être soulignés :

* **Bouton de déclenchement** — `variant="action"` avec `<Spinner v-if />` et un `:disabled` garde. C'est l'affordance « lancez ceci » à l'échelle du projet.
* **Entrées à l'épreuve d'AutoFill** — chaque `Input` porte les six attributs affichés ci-dessus, et le placeholder évite le mot « adresse » (et ses traductions) parce que QuickType d'iOS se base sur le mot lui-même même avec `autocomplete="off"`.
* **`console.*` convient ici.** Le frontend l'utilise ; seuls les fichiers backend sont concernés par la restriction.
* **Aucun changement d'état→couleur fait à la main.** Si votre outil associe un état métier à une couleur, passez par `composables/use-status-tone.js`.
* **Chaque chaîne est un `t()` appel.** Rien de visible par l'utilisateur n'est codé en dur.

D'autres modèles canoniques — cartes d'état, indicateurs, tableaux vs listes, en-têtes de dialogue, motion — sont répertoriés dans [Frontend](/developer/fr/architecture/frontend.md).
{% endstep %}

{% step %}

### Enregistrez l'outil

`frontend/data/tools.js` est la source unique de vérité. Une entrée là-bas vous donne la carte sur la page d'accueil, le tiroir inférieur, la page autonome `/tools/<slug>` et l'élément du menu de navigation — **vous ne touchez pas au routeur**.

{% code title="frontend/data/tools.js" %}

```js
export const ADVANCED_TOOLS = [
  // …entrées existantes…
  { slug: 'certcheck', emoji: '🔐', titleKey: 'certcheck.Title', noteKey: 'advancedtools.CertCheck', component: () => import('@/components/advanced-tools/CertCheck.vue') },
];
```

{% endcode %}

Structure de l'entrée :

<table><thead><tr><th width="230">Champ</th><th>Signification</th></tr></thead><tbody><tr><td><code>slug</code></td><td>Identifiant d'URL stable — <code>/tools/&#x3C;slug></code> et la <code>?tool=&#x3C;slug></code> requête du tiroir.</td></tr><tr><td><code>emoji</code></td><td>Glyphe de la carte et glyphe de l'en-tête du tiroir.</td></tr><tr><td><code>titleKey</code></td><td>Clé i18n pour le titre de l'outil.</td></tr><tr><td><code>noteKey</code></td><td>Clé i18n pour la description en une ligne de la carte.</td></tr><tr><td><code>component</code></td><td>Importation paresseuse du <code>.vue</code> fichier — utilisée à la fois par le tiroir et par la page autonome.</td></tr><tr><td><code>requiresOriginalSite</code></td><td>Facultatif. <code>true</code> cache l'outil sur les instances auto-hébergées. Omettez-le pour un outil public.</td></tr></tbody></table>

L'ordre du tableau correspond à l'ordre des cartes sur la page d'accueil.

{% hint style="info" %}
**`requiresOriginalSite: true`** est destiné aux outils qui dépendent de l'API privée IPCheck.ing et d'un compte connecté — ils sont masqués sur les forks et les instances auto-hébergées, qui n'ont aucun moyen d'atteindre ce backend. Voir [Fonctionnalités liées à IPCheck.ing](/developer/fr/configuration/features-tied-to-ipcheck-ing.md). La plupart des nouveaux outils devraient omettre ce champ.
{% endhint %}
{% endstep %}

{% step %}

### Ajoutez le texte — dans les quatre locales

Chaque chaîne que vous avez référencée a besoin d'une entrée dans **`en.json`, `zh.json`, `fr.json`, et `ru.json`** dans `frontend/locales/`. Les quatre, dans le même changement. Ce n'est pas une tâche de suivi.

Deux endroits à modifier par locale — l'espace de noms propre à votre outil :

{% code title="frontend/locales/en.json" %}

```json
"certcheck": {
  "Title": "Vérification de certificat",
  "Note": "Consultez le certificat TLS de n'importe quel domaine : émetteur, période de validité et noms alternatifs du sujet.",
  "Note2": "Entrez un domaine pour commencer la vérification :",
  "Placeholder": "example.com",
  "fetchError": "Impossible de récupérer les détails du certificat"
}
```

{% endcode %}

…et la description de la carte dans l'espace de noms partagé `advancedtools`  :

{% code title="frontend/locales/en.json" %}

```json
"advancedtools": {
  "CertCheck": "Inspecter le certificat TLS d'un domaine"
}
```

{% endcode %}

L'espace de noms correspond normalement au slug (`macchecker`, `dnsresolver`, `censorshipcheck`). Conservez les mêmes noms de clés dans les quatre fichiers — seules les valeurs changent.

Détails sur le chargement, les sous-packages et la chaîne de repli : [i18n](/developer/fr/development/i18n.md).
{% endstep %}

{% step %}

### Ajouter une entrée au journal des modifications

Ajoutez votre entrée au **dernier** bloc de version dans `frontend/data/changelog.json` — le fichier est parcouru du plus ancien au plus récent et l’interface l’affiche à l’envers.

{% code title="frontend/data/changelog.json" %}

```json
{
  "type": "add",
  "change": {
    "en": "Nouvel outil de vérification de certificat : inspectez le certificat TLS de n'importe quel domaine",
    "zh": "Nouvel outil de vérification de certificat : inspectez le certificat TLS de n'importe quel domaine",
    "fr": "Nouvel outil de vérification de certificat : inspectez le certificat TLS de n'importe quel domaine",
    "ru": "Nouvel outil de vérification de certificat : inspectez le certificat TLS de n'importe quel domaine"
  }
}
```

{% endcode %}

`type` doit être l’un des `add`, `improve`, ou `fix`. Les quatre chaînes de langue doivent être présentes et non vides — `tests/changelog.test.js` sinon, la compilation échoue.
{% endstep %}

{% step %}

### Écrivez les tests

Deux types, tous deux dans `tests/`.

**Tests de fumée du handler** vont dans `tests/api-handlers.test.js`, dans un `describe` bloc à côté des autres. N’effectuez des assertions que sur les branches qui renvoient **avant** le premier `fetchUpstream` appel — la suite n’atteint jamais un service amont réel :

{% code title="tests/api-handlers.test.js" %}

```js
import certCheckHandler from '../api/cert-check.js';

// -- gestionnaire cert-check ---------------------------------------------------
// La présence et la forme du domaine sont imposées par le middleware requireValidDomain
// (tests/guards.test.js) ; les propres branches pré-fetch du gestionnaire sont le
// garde-fou sur la méthode et le retour anticipé en l’absence de clé API.

describe('cert-check handler', () => {
    it('rejette autre chose que GET avec un 405 avant d’atteindre l’amont', async () => {
        const res = createResponse();
        await certCheckHandler(createRequest({ method: 'POST', query: { domain: 'example.com' } }), res);
        assert.equal(res.statusCode, 405);
        assert.equal(res.body.error, 'Method Not Allowed');
    });

    it('renvoie 500 lorsque la clé API est absente', async () => {
        delete process.env.CERT_API_KEY;
        const res = createResponse();
        await certCheckHandler(createRequest({ query: { domain: 'example.com' } }), res);
        assert.equal(res.statusCode, 500);
        assert.equal(res.body.error, 'API key missing');
    });
});
```

{% endcode %}

Le fichier fournit déjà `createRequest()` / `createResponse()` des stubs. Si votre gestionnaire lit une nouvelle variable d’environnement, ajoutez son nom au `ENV_KEYS` tableau en haut afin que les hooks de sauvegarde/restauration la prennent en charge.

**Spécifications unitaires** couvrent toute logique pure que votre outil introduit — validateurs, analyseurs, transformations, un composable avec des entrées simulables. Donnez à chacun son propre `tests/<subject>.test.js`. Si vous avez ajouté un garde-fou, étendez `tests/guards.test.js`.

Qu’est-ce qui *ne* reçoit pas de test : le rendu Vue, les appels réseau réels, les API du navigateur. Voir [Tests](/developer/fr/development/testing.md).
{% endstep %}

{% step %}

### Exécutez l’auto-vérification

```bash
pnpm check
```

Des tests plus une compilation de production. Tout doit être au vert avant d’ouvrir une PR.

Ensuite, examinez l’outil vous-même dans `pnpm dev` — sur la grille de cartes de la page d’accueil, dans le tiroir, et à `/tools/certcheck` — car rien de tout cela n’est vérifiable automatiquement. Mentionnez-le dans la description de la PR.
{% endstep %}
{% endstepper %}

***

## Liste de contrôle

| Étape                                                        | Fichier                                            |
| ------------------------------------------------------------ | -------------------------------------------------- |
| Gestionnaire                                                 | `api/cert-check.js`                                |
| Garde-fou (uniquement en cas de nouvelle forme de paramètre) | `common/guards.js`                                 |
| Route                                                        | `backend-server.js`                                |
| Composant                                                    | `frontend/components/advanced-tools/CertCheck.vue` |
| Entrée du registre                                           | `frontend/data/tools.js`                           |
| Copie × 4                                                    | `frontend/locales/{en,zh,fr,ru}.json`              |
| Journal des modifications × 4                                | `frontend/data/changelog.json`                     |
| Test de fumée                                                | `tests/api-handlers.test.js`                       |
| Spécifications unitaires                                     | `tests/*.test.js`                                  |
| Nouvelle variable d’environnement                            | `.env.example`                                     |

## Aller plus loin

Deux systèmes optionnels auxquels votre outil peut se brancher, tous deux pilotés par des événements — les composants émettent sur le `utils/app-events.js` bus et n’appellent jamais les systèmes directement :

* **Succès.** Émettez un événement de domaine, mappez-le à un identifiant de succès dans `frontend/data/achievement-rules.js`, puis ajoutez le succès à `frontend/data/achievements.js`.
* **Rapports de diagnostic partageables.** Un test « mon réseau » émet `<domain>:finished` avec son résultat structuré ; un générateur dans `frontend/utils/report-builders.js` le normalise, et `common/report-schema.js` autorise les champs. Les générateurs échouent en douceur, donc une entrée de schéma manquante apparaît comme un champ discrètement absent plutôt que comme une erreur — ajoutez le générateur et l’entrée de schéma dans le même changement.

Les deux sont décrits plus en détail dans [Frontend](/developer/fr/architecture/frontend.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/fr/development/adding-a-new-tool.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
