@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.3

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.
Files changed (76) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +11 -5
  5. package/dist/nodefony/config/config.js +44 -4
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  7. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  8. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  9. package/dist/nodefony/service/audit-logger.js +2 -3
  10. package/dist/nodefony/service/certificates.js +140 -15
  11. package/dist/nodefony/service/error-renderer.js +1 -1
  12. package/dist/nodefony/service/http-kernel.js +221 -47
  13. package/dist/nodefony/service/reverse-proxy.js +336 -0
  14. package/dist/nodefony/service/servers/server-http.js +3 -3
  15. package/dist/nodefony/service/servers/server-https.js +3 -3
  16. package/dist/nodefony/service/servers/server-static.js +4 -7
  17. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  18. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  19. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  20. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  21. package/dist/nodefony/service/upload/upload-service.js +3 -3
  22. package/dist/nodefony/src/context/Context.js +28 -1
  23. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  24. package/dist/nodefony/src/context/forwarded.js +2 -2
  25. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  26. package/dist/nodefony/src/context/http/Request.js +1 -9
  27. package/dist/nodefony/src/context/http/Response.js +16 -1
  28. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  29. package/dist/nodefony/src/context/responseEnded.js +27 -0
  30. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  31. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  32. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  33. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  34. package/dist/nodefony/src/proxy/forward.js +476 -0
  35. package/dist/nodefony/src/proxy/rules.js +182 -0
  36. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  37. package/dist/nodefony/src/session/session.js +28 -12
  38. package/dist/types/index.d.ts +5 -0
  39. package/dist/types/nodefony/config/config.d.ts +16 -0
  40. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  41. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  42. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  43. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  44. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  45. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  46. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  47. package/dist/types/nodefony/service/certificates.d.ts +96 -5
  48. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  49. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  50. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  51. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  52. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  53. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  54. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  55. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  56. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  57. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  58. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  59. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  60. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  61. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  62. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  63. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  64. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  65. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  66. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  67. package/docs/cookies.md +13 -13
  68. package/docs/index.md +5 -1
  69. package/docs/observabilite.md +15 -15
  70. package/docs/rate-limit.md +30 -30
  71. package/docs/reverse-proxy.md +522 -0
  72. package/docs/servers.md +94 -60
  73. package/docs/session.md +61 -61
  74. package/docs/sse.md +283 -0
  75. package/docs/upload.md +23 -23
  76. package/package.json +8 -8
@@ -47,7 +47,6 @@ var Context = class extends Service {
47
47
  secure = false;
48
48
  security = null;
49
49
  cleaned = false;
50
- isControlledAccess = false;
51
50
  validDomain = false;
52
51
  finished = false;
53
52
  pushAllowed = false;
@@ -76,6 +75,14 @@ var Context = class extends Service {
76
75
  session = null;
77
76
  cookieSession = null;
78
77
  user = null;
78
+ /**
79
+ * Identifiant de l'appelant AUTHENTIFIÉ, posé par le pare-feu ; `null` hors
80
+ * zone ou pour un anonyme. Lisible au démontage, hors de la bulle ALS : c'est
81
+ * ce que lisent le journal d'audit et le profileur. Distinct de `user`, qui
82
+ * lie la session à son principal de SESSION — y écrire l'identité d'un jeton
83
+ * rattacherait la session d'un autre compte.
84
+ */
85
+ userIdentifier = null;
79
86
  waitAsync = false;
80
87
  isJson = false;
81
88
  _isHtml = null;
@@ -133,6 +140,7 @@ var Context = class extends Service {
133
140
  _phaseIndex = null;
134
141
  _afterResponseFns = null;
135
142
  _afterResponseFired = false;
143
+ requestStore = null;
136
144
  _abortController = null;
137
145
  metaData = {
138
146
  nodefony: {},
@@ -300,9 +308,28 @@ var Context = class extends Service {
300
308
  if (this.requestId && RequestContext.getRequestId() === void 0) return RequestContext.run({ requestId: this.requestId }, () => super.log(pci, severity, msgid, msg));
301
309
  return super.log(pci, severity, msgid, msg);
302
310
  }
311
+ /**
312
+ * Confie au contexte le magasin `RequestContext` de son unité de travail
313
+ * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
314
+ *
315
+ * Un minuteur ou une promesse armé pendant l'unité de travail capture ce
316
+ * magasin et le garde au-delà de la réponse — avec le contexte, l'identité
317
+ * et le jeton du pare-feu. Le vidage ({@link RequestContext.release}) n'y
318
+ * laisse que la corrélation et le scope refermé.
319
+ *
320
+ * @param store - le magasin passé à `RequestContext.run`
321
+ */
322
+ holdRequestStore(store) {
323
+ this.requestStore = store;
324
+ }
303
325
  clean() {
304
326
  this.cleaned = true;
305
327
  this.httpKernel = null;
328
+ const store = this.requestStore;
329
+ if (store) {
330
+ RequestContext.release(store);
331
+ this.requestStore = null;
332
+ }
306
333
  return super.clean();
307
334
  }
308
335
  /**
@@ -15,12 +15,30 @@ import { escapeRegExp } from "nodefony";
15
15
  * Politique de compilation (sûre, ancrée, ReDoS-free) :
16
16
  * - string sans `*` → match EXACT ancré (`^...$`, le `.` est littéral).
17
17
  * - string avec `*` → wildcard UN label (`*.example.com` → `^[^.]+\.example\.com$`,
18
- * RFC 6125 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
18
+ * RFC 9525 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
19
19
  * - `RegExp` → reprise telle quelle (l'auteur assume l'ancrage).
20
20
  *
21
21
  * Compilation faite UNE fois (boot / enregistrement de route) ; le test par
22
22
  * requête est un simple `RegExp.test` sur une liste pré-compilée (zéro alloc hot-path).
23
23
  */
24
+ /**
25
+ * Nom d'hôte d'une autorité brute (`host[:port]`), sans port — la valeur que
26
+ * rendrait `URL.hostname` pour une autorité déjà canonique, sans en payer le
27
+ * parse (`indexOf` + `slice`, #508). Un littéral IPv6 garde ses crochets
28
+ * (`[::1]:5151` → `[::1]`) : couper au premier `:` le réduirait à `[`.
29
+ *
30
+ * @param host - autorité brute (`Host`), éventuellement absente
31
+ * @returns le nom d'hôte, `""` si l'autorité est absente
32
+ */
33
+ function hostnameOf(host) {
34
+ if (!host) return "";
35
+ if (host.charCodeAt(0) === 91) {
36
+ const end = host.indexOf("]");
37
+ return end === -1 ? host : host.slice(0, end + 1);
38
+ }
39
+ const colon = host.indexOf(":");
40
+ return colon === -1 ? host : host.slice(0, colon);
41
+ }
24
42
  const DEV_LOOPBACK = [
25
43
  "localhost",
26
44
  "127.0.0.1",
@@ -118,4 +136,4 @@ function isDomainAllowed(regAlias, domain) {
118
136
  return false;
119
137
  }
120
138
  //#endregion
121
- export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed, resolveTrustedHostNames };
139
+ export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, hostnameOf, isDomainAllowed, resolveTrustedHostNames };
@@ -3,7 +3,7 @@ import { isIPv4 } from "node:net";
3
3
  //#region nodefony/src/context/forwarded.ts
4
4
  /**
5
5
  * Découpe une chaîne sur `sep` au **niveau supérieur uniquement**, en respectant
6
- * les `quoted-string` RFC 7230 (`value = token / quoted-string`, §4) : un
6
+ * les `quoted-string` RFC 9110 §5.6.4 (`value = token / quoted-string`, RFC 7239 §4) : un
7
7
  * séparateur à l'intérieur de guillemets (ex. un host quoté, un IPv6) n'est PAS
8
8
  * un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
9
9
  */
@@ -40,7 +40,7 @@ function splitTopLevel(input, sep) {
40
40
  out.push(buf);
41
41
  return out;
42
42
  }
43
- /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
43
+ /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 9110 §5.6.4). */
44
44
  function unquote(value) {
45
45
  const v = value.trim();
46
46
  if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
@@ -1,4 +1,5 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
+ import { responseEnded } from "../responseEnded.js";
2
3
  import Cookie from "../../cookies/cookie.js";
3
4
  import Context from "../Context.js";
4
5
  import { sanitizeRequestId } from "../requestId.js";
@@ -7,14 +8,51 @@ import HttpResponse from "./Response.js";
7
8
  import Http2Request from "../http2/Request.js";
8
9
  import Http2Response from "../http2/Response.js";
9
10
  import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
10
- import { extend, finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
11
+ import { openSseStream } from "./SseStream.js";
12
+ import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
11
13
  import url, { URL } from "node:url";
12
14
  //#region nodefony/src/context/http/HttpContext.ts
15
+ /**
16
+ * Même URL (chemin, requête, hôte) sous un autre schéma et un autre port.
17
+ * `port` absent → port par défaut du schéma (omis de l'URL).
18
+ *
19
+ * @param from - URL de la requête.
20
+ * @param scheme - schéma cible.
21
+ * @param port - port du serveur cible, s'il est connu.
22
+ * @returns l'URL absolue de redirection.
23
+ */
24
+ function switchUrlScheme(from, scheme, port) {
25
+ const target = new URL(from.href);
26
+ target.protocol = `${scheme}:`;
27
+ target.port = port === void 0 ? "" : String(port);
28
+ return target.href;
29
+ }
13
30
  const socketActiveContext = /* @__PURE__ */ new WeakMap();
14
31
  const socketTimeoutArmed = /* @__PURE__ */ new WeakSet();
32
+ /**
33
+ * Gestionnaire `timeout` PERMANENT d'une socket h1, fabriqué hors de toute
34
+ * méthode : une fermeture créée dans `HttpContext.setTimeout()` partageait
35
+ * l'environnement d'une autre qui lit `this`, et retenait ainsi le contexte de
36
+ * la PREMIÈRE requête de la connexion tant que celle-ci vivait (#561).
37
+ *
38
+ * @param socket - la socket h1 dont on route le délai d'inactivité
39
+ * @returns l'écouteur à attacher une fois par socket
40
+ */
41
+ function idleSocketTimeoutHandler(socket) {
42
+ return () => {
43
+ const ctx = socketActiveContext.get(socket);
44
+ if (ctx !== void 0 && !ctx.cleaned && !ctx.response.response?.writableEnded) {
45
+ ctx._onTimeout();
46
+ return;
47
+ }
48
+ socket.destroy();
49
+ };
50
+ }
15
51
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
16
52
  var HttpContext = class extends Context {
17
53
  proxy = null;
54
+ /** Socket h1 inscrite dans `socketActiveContext` — retirée par `clean()`. */
55
+ activeSocket = null;
18
56
  isRedirect = false;
19
57
  sended = false;
20
58
  request;
@@ -126,24 +164,39 @@ var HttpContext = class extends Context {
126
164
  if (!res) return;
127
165
  if (this.response.stream) {
128
166
  res.setTimeout(this.response.timeout, () => {
129
- if (!this.response.response?.writableEnded) this._onTimeout();
167
+ if (!responseEnded(res)) this._onTimeout();
130
168
  });
131
169
  return;
132
170
  }
133
171
  const socket = res.socket;
134
172
  if (!socket) return;
135
173
  socketActiveContext.set(socket, this);
174
+ this.activeSocket = socket;
136
175
  const ms = this.response.timeout;
137
176
  if (socket.timeout !== ms) socket.setTimeout(ms);
138
177
  if (!socketTimeoutArmed.has(socket)) {
139
178
  socketTimeoutArmed.add(socket);
140
- socket.on("timeout", () => {
141
- const ctx = socketActiveContext.get(socket);
142
- if (ctx && !ctx.response.response?.writableEnded) ctx._onTimeout();
143
- });
179
+ socket.on("timeout", idleSocketTimeoutHandler(socket));
144
180
  }
145
181
  }
146
182
  /**
183
+ * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
184
+ * persistante survit à la requête, et l'entrée de `socketActiveContext`
185
+ * retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
186
+ * pointe encore sur CE contexte est retirée — une requête suivante sur la
187
+ * même socket a pu la réécrire. Le gestionnaire `timeout` de la socket ne
188
+ * trouve alors rien et prend le chemin « socket inactive → `destroy()` »,
189
+ * celui qu'il prenait déjà pour un contexte nettoyé.
190
+ */
191
+ clean() {
192
+ const socket = this.activeSocket;
193
+ if (socket) {
194
+ if (socketActiveContext.get(socket) === this) socketActiveContext.delete(socket);
195
+ this.activeSocket = null;
196
+ }
197
+ super.clean();
198
+ }
199
+ /**
147
200
  * Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
148
201
  * en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
149
202
  *
@@ -229,7 +282,7 @@ var HttpContext = class extends Context {
229
282
  }
230
283
  /** Hook `onSend` (attendu seulement s'il a un écouteur), puis en-têtes et corps. */
231
284
  emitAndWrite(body, encoding) {
232
- if (body) this.response.setBody(body);
285
+ if (body || body === "") this.response.setBody(body);
233
286
  if (this.listenerCount("onSend") > 0) return this.fireAsync("onSend", this.response, this).then(() => this.writeHeadAndBody(body, encoding));
234
287
  return this.writeHeadAndBody(body, encoding);
235
288
  }
@@ -265,6 +318,18 @@ var HttpContext = class extends Context {
265
318
  return this.close();
266
319
  });
267
320
  }
321
+ /**
322
+ * Répond par un flux d'événements serveur (`text/event-stream`).
323
+ *
324
+ * La requête ne se termine qu'à `close()` du flux, ou au départ du client.
325
+ *
326
+ * @param options - battement de cœur, délai `retry:` initial.
327
+ * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
328
+ * @throws HttpError 500 quand la réponse est déjà partie.
329
+ */
330
+ openSse(options) {
331
+ return openSseStream(this, options);
332
+ }
268
333
  flush(chunk, encoding) {
269
334
  return this.response.flush(chunk, encoding);
270
335
  }
@@ -288,41 +353,27 @@ var HttpContext = class extends Context {
288
353
  if (typeof Url === "object") return this.response.redirect(url.format(Url), status, headers);
289
354
  return this.response.redirect(Url, status, headers);
290
355
  }
356
+ /**
357
+ * Redirige vers la même URL en HTTPS, sur le port du serveur HTTPS lié —
358
+ * ou le port par défaut derrière un proxy, dont le port public est inconnu.
359
+ *
360
+ * @param status - code de redirection (défaut de `redirect`).
361
+ * @param headers - en-têtes ajoutés à la réponse.
362
+ */
291
363
  redirectHttps(status, headers) {
292
- if (this.session) {}
293
- let urlExtend = null;
294
- if (this.proxy) urlExtend = {
295
- protocol: "https",
296
- href: "",
297
- host: ""
298
- };
299
- else urlExtend = {
300
- protocol: "https",
301
- port: this.httpKernel?.httpsPort || 443,
302
- href: "",
303
- host: ""
304
- };
305
- const urlChange = extend({}, this.request.url, urlExtend);
306
- const newUrl = url.format(urlChange);
307
- return this.redirect(newUrl, status, headers);
364
+ const port = this.proxy ? void 0 : this.httpKernel?.httpsPort;
365
+ return this.redirect(switchUrlScheme(this.request.url, "https", port), status, headers);
308
366
  }
367
+ /**
368
+ * Redirige vers la même URL en HTTP, sur le port du serveur HTTP lié — ou
369
+ * le port par défaut derrière un proxy.
370
+ *
371
+ * @param status - code de redirection (défaut de `redirect`).
372
+ * @param headers - en-têtes ajoutés à la réponse.
373
+ */
309
374
  redirectHttp(status, headers) {
310
- if (this.session) {}
311
- let urlExtend = null;
312
- if (this.proxy) urlExtend = {
313
- protocol: "http",
314
- href: "",
315
- host: ""
316
- };
317
- else urlExtend = {
318
- protocol: "http",
319
- port: this.httpKernel?.httpPort || 80,
320
- href: "",
321
- host: ""
322
- };
323
- const urlChange = extend({}, this.request.url, urlExtend);
324
- const newUrl = url.format(urlChange);
325
- return this.redirect(newUrl, status, headers);
375
+ const port = this.proxy ? void 0 : this.httpKernel?.httpPort;
376
+ return this.redirect(switchUrlScheme(this.request.url, "http", port), status, headers);
326
377
  }
327
378
  getHostName() {
328
379
  return this.request.getHostName();
@@ -364,4 +415,4 @@ var HttpContext = class extends Context {
364
415
  }
365
416
  };
366
417
  //#endregion
367
- export { HttpContext as default };
418
+ export { HttpContext as default, switchUrlScheme };
@@ -1,5 +1,6 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
2
  import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
3
+ import { hostnameOf } from "../domainMatcher.js";
3
4
  import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
4
5
  import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
5
6
  import { RequestContext, extend, useConfig } from "nodefony";
@@ -52,15 +53,6 @@ function perfMark(field) {
52
53
  const p = globalThis.__nfPerfProbe;
53
54
  if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
54
55
  }
55
- /**
56
- * Nom d'hôte d'une autorité brute (`host[:port]`) : tout ce qui précède le
57
- * premier `:`. Même résultat que `host.split(":")[0]`, sans tableau (#508).
58
- */
59
- function hostnameOf(host) {
60
- if (!host) return "";
61
- const colon = host.indexOf(":");
62
- return colon === -1 ? host : host.slice(0, colon);
63
- }
64
56
  var HttpRequest = class {
65
57
  context;
66
58
  request;
@@ -62,9 +62,24 @@ var HttpResponse = class {
62
62
  * n'a été choisi (ni négociation, ni `render`, ni controller), émet le défaut
63
63
  * `this.contentType` (application/octet-stream) — comportement identique à
64
64
  * l'ancienne pose au constructeur, sans le ping-pong set/remove/re-set.
65
+ *
66
+ * Une réponse SANS corps n'annonce aucun type : `Content-Type` décrit le
67
+ * contenu (RFC 9110 §8.3), et il n'y en a pas.
65
68
  */
66
69
  ensureContentTypeHeader() {
67
- if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
70
+ if (this.response && !this.response.hasHeader("content-type") && !this.#isBodyless()) this.response.setHeader("content-type", this.contentType);
71
+ }
72
+ /**
73
+ * Vrai quand la réponse n'a pas de corps : 204 et 304 n'en ont jamais
74
+ * (RFC 9110 §15.3.5, §15.4.5) ; ailleurs, un corps posé et VIDE. Un corps
75
+ * jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD` rend
76
+ * les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
77
+ */
78
+ #isBodyless() {
79
+ if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return true;
80
+ if (this.context.method === "HEAD") return false;
81
+ if (this.#text !== null) return this.#text === "" && !this.flushing;
82
+ return this.#buffer?.length === 0 && !this.flushing;
68
83
  }
69
84
  /**
70
85
  * Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la