@nodefony/devkit 10.0.0-beta.2 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +27 -163
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/card.js +1 -2
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +217 -20
- package/package.json +17 -12
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
- package/skills/nodefony-add-service/SKILL.md +14 -3
- package/skills/nodefony-authentication/SKILL.md +234 -0
- package/skills/nodefony-browser/SKILL.md +65 -21
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/references/sondes.md +13 -0
- package/skills/nodefony-browser/scripts/audit.mjs +59 -10
- package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
- package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
- package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +78 -82
- package/skills/nodefony-dev/scripts/docs.mjs +24 -8
- package/skills/nodefony-devops/SKILL.md +98 -74
- package/skills/nodefony-devops/references/compose.md +8 -0
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +282 -48
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +30 -5
- package/skills/nodefony-devops/references/variables.md +66 -31
- package/skills/nodefony-migrate-schema/SKILL.md +5 -2
- package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
- package/skills/nodefony-protect-route/SKILL.md +124 -21
- package/skills/nodefony-security-review/SKILL.md +127 -0
- package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
- package/skills/nodefony-troubleshoot/SKILL.md +225 -0
- package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
- package/skills/nodefony-upgrade/SKILL.md +130 -0
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-authentication
|
|
3
|
+
metadata:
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: >
|
|
6
|
+
Établit QUI est l'utilisateur d'une application Nodefony par ce que le framework sert déjà —
|
|
7
|
+
page /login et son habillage, client NodefonyLogin, route me, second facteur, connexion
|
|
8
|
+
récente, retour sûr après connexion, fournisseurs (Keycloak, GitHub), jeton d'un programme —
|
|
9
|
+
au lieu de le réécrire. À charger AVANT d'écrire un formulaire de connexion, une redirection
|
|
10
|
+
après login, une lecture de session ou un appel d'API depuis un script. Ce qu'un utilisateur connecté a le DROIT de faire → nodefony-protect-route.
|
|
11
|
+
Déclencheurs : "ajoute une page de connexion", "formulaire de login", "se connecter", "second
|
|
12
|
+
facteur", "TOTP", "MFA", "exiger une connexion récente", "ramener l'utilisateur après la
|
|
13
|
+
connexion", "?from=", "qui est connecté", "déconnexion", "Keycloak", "se connecter avec
|
|
14
|
+
GitHub", "mon script doit appeler l'API sans formulaire", "jeton d'accès", "202 au login".
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# authentication — la connexion est servie, elle ne s'écrit pas
|
|
18
|
+
|
|
19
|
+
> 🧭 **Tu es arrivé ici directement ? Charge aussi `nodefony-dev`** — il porte la conduite
|
|
20
|
+
> commune (par où commencer, comment prouver que c'est fait). Cette page-ci ne couvre QUE la
|
|
21
|
+
> connexion.
|
|
22
|
+
>
|
|
23
|
+
> Une réponse te manque ? Elle est probablement INSTALLÉE — `rg` ne descend pas dans
|
|
24
|
+
> `node_modules`. `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`
|
|
25
|
+
> la trouve, avec la ligne exacte.
|
|
26
|
+
|
|
27
|
+
> **Maintenance** : vérité courante, jamais un journal. Éditer en place ; historique = `git log`.
|
|
28
|
+
|
|
29
|
+
## 1. Quand m'utiliser / quand passer la main
|
|
30
|
+
|
|
31
|
+
| Besoin | Skill |
|
|
32
|
+
| --------------------------------------------------------------------- | ------------------------------- |
|
|
33
|
+
| Se connecter, se déconnecter, savoir qui est connecté, second facteur | ici |
|
|
34
|
+
| Réserver une route à un rôle, renvoyer l'anonyme vers `/login` (zone) | `nodefony-protect-route` |
|
|
35
|
+
| La ZONE qui reçoit un programme (`machine`, `partner`), un partenaire | `nodefony-protect-route` |
|
|
36
|
+
| Une page qui affiche l'état de connexion en temps réel | `nodefony-add-realtime-channel` |
|
|
37
|
+
| Vérifier à l'écran que la connexion aboutit | `nodefony-browser` |
|
|
38
|
+
|
|
39
|
+
## 2. 🔴 Ce qui est DÉJÀ servi — n'écris ni page, ni controller de compte
|
|
40
|
+
|
|
41
|
+
Une application qui charge `@nodefony/security` sert tout ceci :
|
|
42
|
+
|
|
43
|
+
| Ce qui existe | Ce que ça fait |
|
|
44
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
45
|
+
| `GET /login` | la page : identifiant, mot de passe, second facteur, passkey, boutons des fournisseurs |
|
|
46
|
+
| `POST /nodefony/security/api/auth/login` | `{username, password}` → `200` + cookie ; **`202` = second facteur requis, PAS encore connecté** |
|
|
47
|
+
| `POST /nodefony/security/api/auth/login/totp` | `{code}` — le code à 6 chiffres (ou un code de récupération) qui achève une connexion en `202` |
|
|
48
|
+
| `GET /nodefony/security/api/auth/me` | **TOUJOURS `200`** : `{ user: null }` sans session, sinon `{ user: { id, identifier, roles } }` |
|
|
49
|
+
| `POST /nodefony/security/api/auth/logout` | idempotent ; rend l'adresse de déconnexion du fournisseur quand la session venait de lui |
|
|
50
|
+
| `/nodefony/security/api/totp/{enroll,confirm,disable,status}` | le titulaire connecté active ou retire SON second facteur |
|
|
51
|
+
|
|
52
|
+
- 🔴 **Qui est connecté, côté serveur — deux lignes à retenir, rien d'autre :**
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
this.user; // dans un controller
|
|
56
|
+
RequestContext.getAuthenticatedUser(); // partout ailleurs (service, hook)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Les deux rendent l'utilisateur connecté, ou `null`. C'est la règle UNIQUE du framework : elle
|
|
60
|
+
part du jeton que pose le pare-feu, et ses variantes en dérivent — `RequestContext.getUserId()`
|
|
61
|
+
pour l'`id` seul, `authenticatedUser()` de `@nodefony/user` pour le compte typé `IUser`. Ne lis
|
|
62
|
+
jamais la session à la main, et pas `RequestContext.getUser()`, qui rend aussi l'anonyme.
|
|
63
|
+
|
|
64
|
+
- `me` ne teste jamais une session par son code HTTP : il n'y a pas de `401` à attendre. Le
|
|
65
|
+
champ est `identifier`, pas `username`. Le rechargement d'une page (F5) se restaure avec `me`,
|
|
66
|
+
jamais avec un état gardé côté navigateur.
|
|
67
|
+
- Ces routes sont montées par `@nodefony/framework`, sous un chemin qui porte le nom de
|
|
68
|
+
`security` : c'est le piège qui fait conclure qu'elles n'existent pas.
|
|
69
|
+
- Aucune commande CLI n'active le second facteur d'un compte : c'est le titulaire, connecté, qui
|
|
70
|
+
l'enrôle (`enroll` rend `{secretBase32, otpauthUri}`, `confirm` le valide par un premier code).
|
|
71
|
+
|
|
72
|
+
## 3. Habiller la page `/login` — sans la réécrire
|
|
73
|
+
|
|
74
|
+
Quatre niveaux, du plus léger au plus lourd ; prends le premier qui suffit. La section
|
|
75
|
+
`security.loginPage` est **stricte** : une clé mal orthographiée arrête le démarrage.
|
|
76
|
+
|
|
77
|
+
| Niveau | Geste |
|
|
78
|
+
| ------ | ---------------------------------------------------------------------------------------- |
|
|
79
|
+
| 1 | `skin` (neuf habillages) et `layout` |
|
|
80
|
+
| 2 | `title`, `heading`, `subtitle`, `logo`, `hero`, `footer` |
|
|
81
|
+
| 3 | `stylesheet` : TA feuille, qui redéfinit les variables `--nf-login-*` |
|
|
82
|
+
| 4 | `template` : un gabarit `.eta` à toi — dernier recours, il ne suit plus les mises à jour |
|
|
83
|
+
|
|
84
|
+
`path` (défaut `/login`) déplace la page ; **toute** redirection du framework la suit, y compris
|
|
85
|
+
l'échec d'un fournisseur quand `oauth2.failureRedirect` est omis. N'écris donc jamais `"/login"`
|
|
86
|
+
en dur dans ton code : `loginPagePath(from)` le compose. Détail :
|
|
87
|
+
`node_modules/@nodefony/security/docs/login-page.md`.
|
|
88
|
+
|
|
89
|
+
❌ Recopier la feuille de style de la page, ou un nouveau `.eta` pour changer trois couleurs :
|
|
90
|
+
la page cesse de suivre le framework, et le niveau 1 ou 3 suffisait.
|
|
91
|
+
|
|
92
|
+
## 4. Une connexion dans TON front — `NodefonyLogin`, jamais `fetch`
|
|
93
|
+
|
|
94
|
+
Le plus souvent, un **bouton** suffit : un lien vers `loginPagePath(from)` (`nodefony/client`),
|
|
95
|
+
qui compose `/login?from=<page>`. La page servie fait le reste.
|
|
96
|
+
|
|
97
|
+
Un formulaire à toi se bâtit sur `NodefonyLogin` (`nodefony/client`), qui conduit mot de passe,
|
|
98
|
+
second facteur, passkey, fournisseurs et blocage — par la liaison de TON moteur :
|
|
99
|
+
|
|
100
|
+
| Moteur | Liaison | Import |
|
|
101
|
+
| ------- | --------------------- | ------------------ |
|
|
102
|
+
| React | `useNodefonyLogin` | `nodefony/react` |
|
|
103
|
+
| Vue | `useNodefonyLogin` | `nodefony/vue` |
|
|
104
|
+
| Svelte | `nodefonyLogin` | `nodefony/svelte` |
|
|
105
|
+
| Angular | `injectNodefonyLogin` | `nodefony/angular` |
|
|
106
|
+
|
|
107
|
+
Elle expose l'état (`getState`, `subscribe`) et les gestes : `login`, `submitMfaCode`,
|
|
108
|
+
`loginWithPasskey`, `loadProviders`, `startProvider`, `me`, `logout`. **Suis l'adresse que rend
|
|
109
|
+
`logout()`** : sinon la session du fournisseur (Keycloak) reste ouverte. Détail :
|
|
110
|
+
`node_modules/nodefony/docs/client.md` (§ NodefonyLogin).
|
|
111
|
+
|
|
112
|
+
🔴 **Jamais de `fetch` ni d'`axios` vers `/nodefony/security/api/auth/*`** : le `202`, le
|
|
113
|
+
blocage après échecs, le jeton anti-falsification et la passkey sont autant de cas qu'un
|
|
114
|
+
`fetch` écrit à la main oublie — et chacun est une faille ou une panne.
|
|
115
|
+
|
|
116
|
+
## 5. Le `202` est un DÉFI, pas un succès
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{ "mfaRequired": true, "methods": ["totp"] }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
La session ne porte alors qu'un défi en attente : **l'utilisateur n'est pas connecté**. La liste
|
|
123
|
+
`methods` est **ouverte** — d'autres méthodes viendront ; teste l'appartenance
|
|
124
|
+
(`methods.includes(MFA_METHOD_TOTP)`), jamais l'égalité du tableau. Le code s'envoie à
|
|
125
|
+
`login/totp` (ou `submitMfaCode` avec la liaison).
|
|
126
|
+
|
|
127
|
+
## 6. Ramener l'utilisateur après la connexion — `safeRedirectPath`
|
|
128
|
+
|
|
129
|
+
Le renvoi de l'anonyme vers la page se déclare sur la ZONE (`entryPoint`, skill
|
|
130
|
+
`nodefony-protect-route`) ; il arrive avec `?from=<page>`. Au retour :
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { safeRedirectPath } from "nodefony"; // aussi dans nodefony/client
|
|
134
|
+
const destination = safeRedirectPath(from); // "/" par défaut
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
🔴 **Un `from` ne se suit JAMAIS sans `safeRedirectPath`.** C'est la seule garde contre la
|
|
138
|
+
redirection ouverte : `?from=https://site-piege.example` ou `?from=//site-piege.example`
|
|
139
|
+
enverrait ton utilisateur ailleurs, connecté. N'écris pas ta propre garde
|
|
140
|
+
(`startsWith("/")`, expression régulière d'URL) : `//hôte` commence par `/`, et c'est
|
|
141
|
+
exactement le contournement qu'elle laisse passer.
|
|
142
|
+
|
|
143
|
+
## 7. Exiger une connexion RÉCENTE, ou deux facteurs, avant une action sensible
|
|
144
|
+
|
|
145
|
+
Une étiquette sur la route — n'écris PAS la vérification :
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { Post, RequireAuthentication } from "@nodefony/framework";
|
|
149
|
+
|
|
150
|
+
@Post("/transfers")
|
|
151
|
+
@RequireAuthentication({ maxAge: 300, amr: ["otp"] }) // < 5 min, avec le code TOTP
|
|
152
|
+
transfer() { … }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `maxAge` en secondes, `amr` = moyens exigés (tous présents) ; l'un ou l'autre suffit.
|
|
156
|
+
- Refus = **401** dont le `WWW-Authenticate` porte `error="insufficient_user_authentication"`
|
|
157
|
+
(RFC 9470) : « reconnecte-toi », pas « interdit ». Évalué APRÈS `@IsGranted` : un rôle absent
|
|
158
|
+
reste un 403.
|
|
159
|
+
- Pour LIRE l'information (l'afficher, la journaliser) : `readSessionAuthentication(this.session)`
|
|
160
|
+
(`nodefony`) → `{ at, amr }` ou `null`. Jamais pour décider à la place de l'étiquette.
|
|
161
|
+
|
|
162
|
+
| Connexion par | `amr` |
|
|
163
|
+
| ------------------- | ---------------- |
|
|
164
|
+
| mot de passe | `["pwd"]` |
|
|
165
|
+
| mot de passe + TOTP | `["pwd", "otp"]` |
|
|
166
|
+
| passkey | `["pop"]` |
|
|
167
|
+
| fournisseur | `[]` |
|
|
168
|
+
|
|
169
|
+
- Session sans ces métadonnées (ouverte avant, ou altérée) : l'étiquette **refuse**, elle ne devine pas.
|
|
170
|
+
- ❌ `session.getMetaBag("amr")`, ❌ un `if (Date.now() - auth.at > …)` écrit dans l'action : tu
|
|
171
|
+
recopies une garde que le framework porte, avec ses refus normés.
|
|
172
|
+
- La route doit être sous une zone `session` : hors zone, aucune session n'est reprise et
|
|
173
|
+
l'étiquette refuse tout le monde.
|
|
174
|
+
|
|
175
|
+
## 8. Les routes de connexion restent sous le contrôle anti-falsification
|
|
176
|
+
|
|
177
|
+
`bypassFirewall` sort une route de l'authentification, **pas** du contrôle CSRF : une route de
|
|
178
|
+
connexion à toi reste soumise au jeton. Seul `@CsrfExempt` l'en dispense — et il ouvrirait la
|
|
179
|
+
connexion à n'importe quel site (connexion forcée). Garde le jeton ; `NodefonyLogin` le porte.
|
|
180
|
+
|
|
181
|
+
## 9. 🔴 En production, un mot de passe en HTTP clair est refusé (`403`)
|
|
182
|
+
|
|
183
|
+
Avant toute vérification, et la page affiche « doit passer par HTTPS ». Derrière un proxy qui
|
|
184
|
+
termine TLS, déclare-le dans `trustProxy` (config de `@nodefony/http`) — sinon **toutes** les
|
|
185
|
+
connexions échouent. `security.allowInsecureCredentials` n'est pas la correction : elle accepte
|
|
186
|
+
le clair.
|
|
187
|
+
|
|
188
|
+
## 10. Se connecter par un fournisseur (Keycloak, GitHub…)
|
|
189
|
+
|
|
190
|
+
Le fournisseur se déclare dans `security.oauth2.providers` ; ses identifiants viennent de
|
|
191
|
+
l'environnement (`NF_KEYCLOAK_*`, `GITHUB_CLIENT_ID`…). Deux commandes, faute de quoi on
|
|
192
|
+
diagnostique un `401` à l'aveugle :
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx nodefony security:oauth:doctor [-p keycloak] # découverte, URL de retour, secret
|
|
196
|
+
npx nodefony security:keycloak:realm [--write|--check|--apply] # realm dérivé de TA config
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--write` écrit le realm (rôles, audiences) dans `docker/keycloak/import/`, `--check` sort en 1
|
|
200
|
+
s'il ne suit plus la configuration, `--apply` pose le thème de connexion sur le Keycloak de
|
|
201
|
+
développement. Un `401` avec un jeton Keycloak valide sur une API, c'est presque toujours
|
|
202
|
+
l'audience absente : `--write`, puis réimport. Détail :
|
|
203
|
+
`node_modules/@nodefony/security/docs/keycloak.md`.
|
|
204
|
+
|
|
205
|
+
## 11. Un PROGRAMME ne remplit jamais le formulaire
|
|
206
|
+
|
|
207
|
+
Un script, une tâche planifiée, un agent n'ont ni navigateur ni cookie : ils présentent une
|
|
208
|
+
preuve à chaque appel, `Authorization: Bearer …`.
|
|
209
|
+
|
|
210
|
+
| Preuve | L'obtenir |
|
|
211
|
+
| ------------------------------- | ------------------------------------------------------------------- |
|
|
212
|
+
| clé d'API opaque (`nf_…`) | `POST /nodefony/security/api/keys` (session d'administration) |
|
|
213
|
+
| jeton signé par l'application | `npx nodefony security:mcp:token [compte] --scope … --ttl … --json` |
|
|
214
|
+
| jeton Keycloak (client machine) | `requestClientCredentialsToken` de `@nodefony/security` |
|
|
215
|
+
|
|
216
|
+
❌ Un `POST` vers la route de login depuis un script, ou un cookie de session recopié dans sa
|
|
217
|
+
configuration : le premier fait stocker un mot de passe au programme, le second expire et casse
|
|
218
|
+
en silence. La zone qui reçoit ces preuves (`machine`, `partner`, `stateless: true`) et la
|
|
219
|
+
commande qui l'éprouve (`app:partner:call`) → skill `nodefony-protect-route`. Détail :
|
|
220
|
+
`node_modules/@nodefony/security/docs/api-keys.md`, `obtenir-un-jeton.md`.
|
|
221
|
+
|
|
222
|
+
## 12. Prouver que c'est fait
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# la page est servie, avec ton habillage (lis son <title>)
|
|
226
|
+
curl -s localhost:5151/login
|
|
227
|
+
# me répond 200 sans session — teste user, pas le code
|
|
228
|
+
curl -s localhost:5151/nodefony/security/api/auth/me
|
|
229
|
+
# le retour refuse une destination étrangère : les deux doivent rendre "/"
|
|
230
|
+
node -e 'import("nodefony").then(({safeRedirectPath:s})=>console.log(s("//evil.example"),s("https://evil.example")))'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Une action « connexion récente » se prouve par trois sessions : mot de passe seul → refusée ;
|
|
234
|
+
mot de passe + TOTP → servie ; la même, ouverte depuis plus longtemps que le délai → refusée.
|
|
@@ -178,7 +178,7 @@ pour un élément contre son fond. La sonde ne connaît donc aucun sélecteur
|
|
|
178
178
|
composants.
|
|
179
179
|
|
|
180
180
|
Réglages par variables d'environnement : `NF_BROWSER_BASE`, `NF_BROWSER_PAGE`, `NF_BROWSER_EXPECT`,
|
|
181
|
-
`NF_BROWSER_LOGIN`, `NF_BROWSER_USER`, `NF_BROWSER_PASSWORD`, `NF_BROWSER_PROBES`
|
|
181
|
+
`NF_BROWSER_LOGIN`, `NF_BROWSER_USER`, `NF_BROWSER_PASSWORD`, `NF_BROWSER_WHOAMI`, `NF_BROWSER_PROBES`
|
|
182
182
|
(`libellé=sélecteur`, séparés par des virgules), `NF_BROWSER_FAMILIES`, `NF_BROWSER_WIDTHS`,
|
|
183
183
|
`NF_BROWSER_SEUIL_LOURD`, `NF_BROWSER_SEUIL_LENT`, `NF_BROWSER_ACTIONS`, `NF_BROWSER_FULLPAGE`. Le détail vit dans l'en-tête de chaque script.
|
|
184
184
|
|
|
@@ -194,8 +194,9 @@ NF_BROWSER_ACTIONS="Nouvelle commande|voir:Moyen de paiement" node node_modules/
|
|
|
194
194
|
|
|
195
195
|
Grammaire : `verbe:cible[=valeur]`, séquence séparée par `|`, verbe facultatif (`clic` par défaut),
|
|
196
196
|
cible cherchée d'abord comme **texte visible** puis comme sélecteur CSS. Verbes : `clic` · `double`
|
|
197
|
-
· `droit` (clic droit) · `survol` · `saisir:Nom=valeur` · `
|
|
198
|
-
·
|
|
197
|
+
· `droit` (clic droit) · `survol` · `saisir:Nom=valeur` · `choisir:Liste=valeur` (une option d'un
|
|
198
|
+
`<select>`, par sa valeur ou son libellé ; la cible peut être le libellé qui l'enveloppe) ·
|
|
199
|
+
`touche:Enter` · `voir` · `defiler:600` · `attendre`.
|
|
199
200
|
|
|
200
201
|
- 🔴 **`voir` plutôt qu'un clic pour amener dans la vue** : sur un formulaire, le texte d'une
|
|
201
202
|
question est un `label` — cliquer dessus coche la case qu'il décrit, et tu observes alors un écran
|
|
@@ -207,9 +208,12 @@ cible cherchée d'abord comme **texte visible** puis comme sélecteur CSS. Verbe
|
|
|
207
208
|
faite sur un écran qu'on n'a pas ouvert est pire qu'aucune mesure.
|
|
208
209
|
|
|
209
210
|
**`NF_BROWSER_LOGIN` n'a pas de défaut** : c'est le chemin du formulaire de connexion de **ton**
|
|
210
|
-
application.
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
application. Une application qui charge `@nodefony/security` sert le sien à **`/login`** (sauf
|
|
212
|
+
`security.loginPage.path` ou `enabled: false`) : pose donc `NF_BROWSER_LOGIN=/login`. La sonde ne
|
|
213
|
+
le devine pas pour autant — deviner l'enverrait, ailleurs, sur une page inexistante, où elle
|
|
214
|
+
mesurerait un écran d'erreur en croyant s'être authentifiée. Sans lui, un identifiant posé fait
|
|
215
|
+
s'arrêter la sonde avec un message — jamais une mesure fausse. Une session reprise se prouve par
|
|
216
|
+
`/nodefony/security/api/auth/me` (`NF_BROWSER_WHOAMI`) : un `{ user: null }` la déclare périmée.
|
|
213
217
|
|
|
214
218
|
Les sondes de couleur cherchent tes sélecteurs, pas ceux d'une bibliothèque : le thème est lu sur le
|
|
215
219
|
`color-scheme` **calculé** (ce que le moteur applique) et sur `data-theme`. Si ton application marque
|
|
@@ -239,6 +243,20 @@ NF_BROWSER_STORAGE="ma-cle-de-theme=light" node .../scripts/inspect.mjs /
|
|
|
239
243
|
Le champ `theme` de la sortie dit ce qui a été RÉELLEMENT appliqué. Vérifie-le : c'est ainsi qu'on
|
|
240
244
|
sait qu'on a mesuré le bon écran.
|
|
241
245
|
|
|
246
|
+
### Mesurer sur l'écran que tu veux — téléphone, bureau, grand écran
|
|
247
|
+
|
|
248
|
+
La sonde mesure en 1 440 × 900 par défaut. Une page juste sur un bureau peut obliger à défiler pour
|
|
249
|
+
atteindre son bouton sur un téléphone, ou flotter perdue sur un écran de 2 560 px :
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
NF_BROWSER_VIEWPORT=mobile-court NF_BROWSER_FAMILIES=pli node .../scripts/inspect.mjs /connexion "Se connecter"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Écrans nommés : `mobile` (390 × 844), `mobile-court` (375 × 667), `tablette` (820 × 1180),
|
|
256
|
+
`bureau` (défaut), `large` (1 920 × 1080), `tres-large` (2 560 × 1440) — ou `LARGEURxHAUTEUR`.
|
|
257
|
+
Sous 600 px de large, l'écran est émulé en **téléphone** (tactile, densité 2), pas seulement rétréci.
|
|
258
|
+
Un nom inconnu est **refusé** (code 64). Le champ `viewport` de la sortie dit l'écran mesuré.
|
|
259
|
+
|
|
242
260
|
## Les familles de sondes — activables, jamais un mur de JSON
|
|
243
261
|
|
|
244
262
|
Le socle ci-dessus sort toujours. Le reste s'active par famille, chacune rendant un **verdict**
|
|
@@ -248,15 +266,16 @@ Le socle ci-dessus sort toujours. Le reste s'active par famille, chacune rendant
|
|
|
248
266
|
NF_BROWSER_FAMILIES=axe,perf,reseau node .../scripts/inspect.mjs /tableau-de-bord "Chiffre d affaires"
|
|
249
267
|
```
|
|
250
268
|
|
|
251
|
-
| Famille | Question à laquelle elle répond
|
|
252
|
-
| ------------ |
|
|
253
|
-
| **`axe`** | **Audit WCAG complet par `axe-core` — une centaine de règles, dont le contraste de TOUT le texte**
|
|
254
|
-
| `a11y` | Étiquettes, noms accessibles, hiérarchie des titres, cibles < 24 px, arbre d'accessibilité
|
|
255
|
-
| `rendu` | Débordement horizontal, éléments hors viewport, polices RÉELLEMENT chargées
|
|
256
|
-
| `reseau` | Requêtes, échecs, ressources lourdes et lentes, octets réellement transférés
|
|
257
|
-
| `perf` | TTFB, FCP, LCP, CLS, tâches longues — verdict sur les seuils Web Vitals
|
|
258
|
-
| `stockage` | Attributs des cookies et inventaire du Web Storage — **jamais les valeurs**
|
|
259
|
-
| `responsive` | Le débordement horizontal rejoué à plusieurs largeurs (`NF_BROWSER_WIDTHS`)
|
|
269
|
+
| Famille | Question à laquelle elle répond |
|
|
270
|
+
| ------------ | ----------------------------------------------------------------------------------------------------- |
|
|
271
|
+
| **`axe`** | **Audit WCAG complet par `axe-core` — une centaine de règles, dont le contraste de TOUT le texte** |
|
|
272
|
+
| `a11y` | Étiquettes, noms accessibles, hiérarchie des titres, cibles < 24 px, arbre d'accessibilité |
|
|
273
|
+
| `rendu` | Débordement horizontal, éléments hors viewport, polices RÉELLEMENT chargées |
|
|
274
|
+
| `reseau` | Requêtes, échecs, ressources lourdes et lentes, octets réellement transférés |
|
|
275
|
+
| `perf` | TTFB, FCP, LCP, CLS, tâches longues — verdict sur les seuils Web Vitals |
|
|
276
|
+
| `stockage` | Attributs des cookies et inventaire du Web Storage — **jamais les valeurs** |
|
|
277
|
+
| `responsive` | Le débordement horizontal rejoué à plusieurs largeurs (`NF_BROWSER_WIDTHS`) |
|
|
278
|
+
| `pli` | Faut-il défiler pour AGIR ? Le bouton principal face au bas de l'écran, la place du bloc qui le porte |
|
|
260
279
|
|
|
261
280
|
`NF_BROWSER_FAMILIES=toutes` active tout ; un nom inconnu est **refusé** (code 64), jamais ignoré.
|
|
262
281
|
Ce que chaque champ veut dire, comment lire un verdict, et **quand chaque famille se trompe** :
|
|
@@ -362,6 +381,26 @@ en bout : le message part-il, revient-il, et l'écran le reçoit-il ?
|
|
|
362
381
|
> lui, vaut pour toute attente que tu écriras : tant qu'elle n'a pas échoué une fois, elle ne
|
|
363
382
|
> discrimine rien.
|
|
364
383
|
|
|
384
|
+
### Éprouver la REPRISE après une coupure réseau — `NF_BROWSER_CUT`
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
docker exec -e NF_BROWSER_CUT=5000:4000 mon-app-browser node /app/see-screen/watch.mjs /tableau-de-bord 25000
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`<début>:<durée>` en millisecondes : le réseau tombe 5 s après l'ouverture de la page, revient 4 s
|
|
391
|
+
plus tard. Chaque socket coupé sort avec `closedAt` et `closeCode` (1006, ce que voit un client
|
|
392
|
+
qui perd le réseau), le bloc `cut` compte les réouvertures refusées pendant la coupure, et le
|
|
393
|
+
socket rouvert montre ses frames : l'accueil, les abonnements rejoués, leur acquittement, puis le
|
|
394
|
+
flux. C'est la seule preuve qu'une page qui perd le réseau **retrouve** ses données.
|
|
395
|
+
|
|
396
|
+
- **Le serveur est prévenu** (fermeture 1000) : la connexion coupée ne reste pas à demi ouverte
|
|
397
|
+
chez lui. Une vraie perte de réseau, elle, ne le prévient pas. La sonde éprouve la reprise du
|
|
398
|
+
client, pas la détection d'un pair mort.
|
|
399
|
+
- **Le relais Vite (`/_vite/`) n'est pas coupé** : son client de rechargement à chaud recharge la
|
|
400
|
+
page quand il retrouve le serveur. Vous observeriez une page neuve, pas sa reprise.
|
|
401
|
+
- **Les `ERR_INTERNET_DISCONNECTED` de la console pendant la coupure sont attendus.**
|
|
402
|
+
- Une valeur malformée est refusée (code 64), jamais ignorée.
|
|
403
|
+
|
|
365
404
|
## Piloter le socket de bout en bout — `socket.mjs`
|
|
366
405
|
|
|
367
406
|
`watch.mjs` regarde le trafic d'une page ; celui-ci **conduit** : il ouvre un socket temps réel
|
|
@@ -387,13 +426,12 @@ Le protocole du fil (les quatre formes de frame), la lecture de chaque verdict
|
|
|
387
426
|
<!-- prettier-ignore -->
|
|
388
427
|
| Contrainte | Ce qui arrive sinon |
|
|
389
428
|
| --- | --- |
|
|
390
|
-
| Joindre l'app par **`host.docker.internal`** | `localhost` désigne le CONTENEUR, pas ta machine.
|
|
429
|
+
| Joindre l'app par **`host.docker.internal`** | `localhost` désigne le CONTENEUR, pas ta machine. Ce nom est dans les `trustedHosts` de développement d'une application générée (`nodefony.config.ts`, bloc http) ; sans lui, la page annonce ses assets en `127.0.0.1` — le conteneur — et, avec `domainCheck`, la barrière répond **`421`**. |
|
|
391
430
|
| Passer par **HTTPS** | Le cookie de session est `secure` : sur une origine `http://` non-`localhost`, le navigateur le **jette**, et tout revient en **`401`** — ce qui se lit à tort comme un login qui rate. |
|
|
392
|
-
|
|
|
431
|
+
| Rien à poser pour **Vite** | Nodefony relaie Vite sur l'origine de la page (`/_vite/…`) : scripts, images et WebSocket du rechargement à chaud suivent l'hôte par lequel tu arrives, en HTTPS si la page l'est — pas de contenu mixte, un seul certificat. |
|
|
393
432
|
|
|
394
|
-
Si la page
|
|
395
|
-
`
|
|
396
|
-
réglage durable gagne toujours sur une déduction.
|
|
433
|
+
Si un script de la page vise encore `127.0.0.1:5173`, le framework installé précède le relais
|
|
434
|
+
(`@nodefony/frontend` à mettre à jour) : les balises d'une version courante sont relatives.
|
|
397
435
|
|
|
398
436
|
## Pièges — chacun a déjà fait conclure faux
|
|
399
437
|
|
|
@@ -418,7 +456,13 @@ réglage durable gagne toujours sur une déduction.
|
|
|
418
456
|
- **Une capture ne s'écrase pas.** Réutiliser un nom laisse l'ancienne image en place pendant que
|
|
419
457
|
l'appel répond « OK » : tu lis un écran périmé. Les sondes horodatent — ne le contourne pas.
|
|
420
458
|
- **Un état d'authentification sauvegardé peut être périmé** (session expirée, serveur redémarré).
|
|
421
|
-
Les sondes le
|
|
459
|
+
Les sondes le PROUVENT avant de mesurer : `NF_BROWSER_WHOAMI` (défaut
|
|
460
|
+
`/nodefony/security/api/auth/me`) doit répondre 2xx SANS `{ user: null }` dans son corps (c'est
|
|
461
|
+
ainsi qu'il dit « anonyme »), sinon l'état est jeté, la reprise est
|
|
462
|
+
annoncée sur la sortie d'erreur et le parcours de connexion est rejoué. Attendre un renvoi vers le
|
|
463
|
+
formulaire ne suffisait pas : une page PUBLIQUE n'en fait jamais, et la sonde mesurait alors en
|
|
464
|
+
anonyme sans le dire. Application sans `@nodefony/security` : pointer `NF_BROWSER_WHOAMI` sur une
|
|
465
|
+
route qui rend 401/403 (ou `{ user: null }`) à un anonyme — ou le laisser vide, et la sonde se connecte à chaque fois.
|
|
422
466
|
- **Chaque compte a SON état sauvegardé** (le fichier porte l'identifiant). C'est ce qui permet
|
|
423
467
|
d'enchaîner deux sondes sous deux comptes — comparer ce que voit un administrateur et ce que voit
|
|
424
468
|
un utilisateur ordinaire — sans que la seconde reprenne la session de la première. Sans cela on
|
|
@@ -36,10 +36,19 @@ Deux frames servent de repères :
|
|
|
36
36
|
|
|
37
37
|
### `welcome`
|
|
38
38
|
|
|
39
|
-
`receivedAfterMs`, canaux, méthodes, identité. **S'il ne vient jamais** (code de retour
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
`verdict`, `receivedAfterMs`, canaux, méthodes, identité. **S'il ne vient jamais** (code de retour
|
|
40
|
+
65), la sortie dit lequel des deux cas :
|
|
41
|
+
|
|
42
|
+
- **`REFUSÉ AU HANDSHAKE`** — le serveur a répondu autre chose que `101` : la socket ne s'est jamais
|
|
43
|
+
ouverte, et la sonde le rend tout de suite avec `status`, `afterMs` et la `browserReason` du
|
|
44
|
+
navigateur (la page ne voit qu'une fermeture `1006` ; le statut, seul le navigateur le connaît).
|
|
45
|
+
`401` : la page n'est pas authentifiée (donner `NF_BROWSER_USER` + `NF_BROWSER_LOGIN`) ; `403` :
|
|
46
|
+
le compte n'a pas le rôle, ou l'`Origin` de la page est refusé ; `404` : le chemin du endpoint
|
|
47
|
+
est faux. Chromium ne nomme pas le code d'un `401` porteur de `WWW-Authenticate` (« HTTP
|
|
48
|
+
Authentication failed ») : la sonde le traduit.
|
|
49
|
+
- **Rien sur la sortie standard, message « en 8000 ms »** — la socket s'est ouverte mais l'accueil
|
|
50
|
+
n'est jamais venu : l'endpoint n'est pas un endpoint temps réel Nodefony, ou le serveur ferme
|
|
51
|
+
après l'ouverture. Le réseau qui « passe » n'innocente rien.
|
|
43
52
|
|
|
44
53
|
### `subscription`
|
|
45
54
|
|
|
@@ -164,6 +164,19 @@ pixels, ni tactile, ni `user-agent`. Une media query sur `pointer` ou `hover` ne
|
|
|
164
164
|
capture PNG est prise AVANT cette famille, à la largeur d'origine : les débordements constatés ici
|
|
165
165
|
ne s'y voient pas.
|
|
166
166
|
|
|
167
|
+
## `pli` — les proportions, sur l'écran demandé
|
|
168
|
+
|
|
169
|
+
Sur l'écran de `NF_BROWSER_VIEWPORT` : la hauteur de la page et le défilement (`scrollPx`), le
|
|
170
|
+
débordement en largeur, l'**action principale** (`primary` : libellé, bas en pixels, `marginPx` =
|
|
171
|
+
marge avant le bas de l'écran) et le **bloc** qui la porte (`frame` : taille, part de la largeur et
|
|
172
|
+
de la hauteur de l'écran). `ALERTE` quand il faut défiler pour atteindre l'action, ou que la page
|
|
173
|
+
déborde en largeur — un pied de page sous le pli n'est PAS une alerte.
|
|
174
|
+
|
|
175
|
+
**Quand elle se trompe** : l'action principale est devinée — le plus GRAND bouton de validation
|
|
176
|
+
visible (un bouton « retour » est aussi un `submit`, jamais le plus large). Sur une page à plusieurs
|
|
177
|
+
formulaires, désigne-la : `NF_BROWSER_PRIMARY="#mon-bouton"`. Le bloc est le plus grand ancêtre de
|
|
178
|
+
l'action resté plus étroit que l'écran : sur une page sans carte, c'est une colonne, pas un cadre.
|
|
179
|
+
|
|
167
180
|
## Lire un verdict sans se faire piéger
|
|
168
181
|
|
|
169
182
|
1. **`ALERTE` n'est pas « cassé »** — c'est « mérite un regard ». Le détail dit lequel.
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `@env` NF_BROWSER_LOGIN chemin du formulaire de connexion — aucun défaut deviné
|
|
20
20
|
* `@env` NF_BROWSER_USER identifiant ; sans lui, aucune authentification n'est tentée
|
|
21
21
|
* `@env` NF_BROWSER_PASSWORD mot de passe associé
|
|
22
|
+
* `@env` NF_BROWSER_COLOR_SCHEME `light` ou `dark` — posé au MOTEUR, seul réglage que l'onglet de Lighthouse reçoit ; sans lui, le thème du système
|
|
22
23
|
* `@env` NF_BROWSER_CATEGORIES catégories à jouer, séparées par des virgules (défaut : toutes celles que ce Lighthouse connaît)
|
|
23
24
|
* `@env` NF_BROWSER_FORMFACTOR `desktop` (défaut) ou `mobile` — un score de performance ne veut RIEN dire sans son appareil
|
|
24
25
|
* `@env` NF_BROWSER_SEUIL_AUDIT score en deçà duquel un audit est retenu, en pourcentage (défaut 90)
|
|
@@ -29,10 +30,22 @@
|
|
|
29
30
|
import { createServer } from "node:net";
|
|
30
31
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
31
32
|
import path from "node:path";
|
|
32
|
-
import { mkdtempSync } from "node:fs";
|
|
33
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
33
34
|
import { tmpdir } from "node:os";
|
|
34
|
-
import {
|
|
35
|
-
|
|
35
|
+
import {
|
|
36
|
+
BASE,
|
|
37
|
+
COLOR_SCHEME,
|
|
38
|
+
LOGIN,
|
|
39
|
+
PASSWORD,
|
|
40
|
+
OUTPUT,
|
|
41
|
+
USER,
|
|
42
|
+
} from "./lib/browser.mjs";
|
|
43
|
+
import {
|
|
44
|
+
browserOrder,
|
|
45
|
+
captureSlug,
|
|
46
|
+
colorSchemeLaunchArgs,
|
|
47
|
+
summarizeLighthouse,
|
|
48
|
+
} from "./lib/probes.mjs";
|
|
36
49
|
|
|
37
50
|
const PAGE = process.argv[2] ?? process.env.NF_BROWSER_PAGE ?? "/";
|
|
38
51
|
const THRESHOLD = Number(process.env.NF_BROWSER_SEUIL_AUDIT ?? 90) / 100;
|
|
@@ -85,11 +98,40 @@ const port = await freePort();
|
|
|
85
98
|
// Profil PERSISTANT : c'est lui qui porte la session entre notre connexion et
|
|
86
99
|
// l'onglet que Lighthouse ouvrira. Jetable — il vit le temps de l'audit.
|
|
87
100
|
const profile = mkdtempSync(path.join(tmpdir(), "nf-audit-"));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
// Le navigateur se CONSTATE, dans le même ordre que les autres sondes
|
|
102
|
+
// (`browserOrder` : celui du pilote, puis ceux DÉJÀ posés sur la machine). Un
|
|
103
|
+
// `channel: "chromium"` codé en dur faisait échouer l'audit sur un poste où
|
|
104
|
+
// seul Chrome est installé — là même où `inspect.mjs` passait.
|
|
105
|
+
let ctx = null;
|
|
106
|
+
const failures = [];
|
|
107
|
+
for (const channel of browserOrder(process.env.NF_BROWSER_ENGINE)) {
|
|
108
|
+
try {
|
|
109
|
+
ctx = await chromium.launchPersistentContext(profile, {
|
|
110
|
+
channel,
|
|
111
|
+
ignoreHTTPSErrors: true,
|
|
112
|
+
// `null` : le pilote n'émule rien — c'est le réglage du moteur qui
|
|
113
|
+
// décide, le même pour la connexion et pour l'onglet de Lighthouse.
|
|
114
|
+
colorScheme: null,
|
|
115
|
+
args: [
|
|
116
|
+
`--remote-debugging-port=${port}`,
|
|
117
|
+
"--no-sandbox",
|
|
118
|
+
...colorSchemeLaunchArgs(COLOR_SCHEME),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
failures.push(`${channel} : ${String(e).split("\n")[0]}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!ctx) {
|
|
127
|
+
console.error(
|
|
128
|
+
`Aucun navigateur n'a répondu :\n ${failures.join("\n ")}\n` +
|
|
129
|
+
"Installer celui du pilote (npx playwright install chromium) ou viser un\n" +
|
|
130
|
+
"navigateur présent : NF_BROWSER_ENGINE=chrome (ou msedge).",
|
|
131
|
+
);
|
|
132
|
+
rmSync(profile, { recursive: true, force: true });
|
|
133
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
134
|
+
}
|
|
93
135
|
|
|
94
136
|
try {
|
|
95
137
|
const page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
@@ -153,17 +195,24 @@ try {
|
|
|
153
195
|
// vérifier — et à comparer dans le temps.
|
|
154
196
|
mkdirSync(OUTPUT, { recursive: true });
|
|
155
197
|
const stamp = new Date().toISOString().replace(/[:.]/gu, "-").slice(0, 19);
|
|
156
|
-
const slug = PAGE
|
|
198
|
+
const slug = captureSlug(PAGE);
|
|
157
199
|
const fullPath = path.join(OUTPUT, `lighthouse-${slug}-${stamp}.json`);
|
|
158
200
|
writeFileSync(fullPath, JSON.stringify(lhr), "utf8");
|
|
159
201
|
|
|
160
202
|
console.log(
|
|
161
203
|
JSON.stringify(
|
|
162
|
-
{
|
|
204
|
+
{
|
|
205
|
+
...summarizeLighthouse(lhr, THRESHOLD),
|
|
206
|
+
colorScheme: COLOR_SCHEME ?? "système",
|
|
207
|
+
fullReport: fullPath,
|
|
208
|
+
},
|
|
163
209
|
null,
|
|
164
210
|
2,
|
|
165
211
|
),
|
|
166
212
|
);
|
|
167
213
|
} finally {
|
|
168
214
|
await ctx.close();
|
|
215
|
+
// Le profil est jetable : fermé, il ne sert plus à rien et resterait dans le
|
|
216
|
+
// dossier temporaire du système.
|
|
217
|
+
rmSync(profile, { recursive: true, force: true });
|
|
169
218
|
}
|