@nodefony/http 10.0.0-beta.4 → 10.0.0-beta.6

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 (103) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -25
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +6 -3
  7. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  8. package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
  9. package/dist/nodefony/service/audit-logger.js +3 -17
  10. package/dist/nodefony/service/error-renderer.js +6 -4
  11. package/dist/nodefony/service/http-kernel.js +191 -40
  12. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  13. package/dist/nodefony/service/request-logger.js +1 -1
  14. package/dist/nodefony/service/reverse-proxy.js +7 -2
  15. package/dist/nodefony/service/servers/clientError.js +28 -4
  16. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  17. package/dist/nodefony/service/servers/server-http.js +5 -3
  18. package/dist/nodefony/service/servers/server-https.js +12 -5
  19. package/dist/nodefony/service/servers/server-static.js +1 -6
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  21. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  22. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  23. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  24. package/dist/nodefony/service/upload/upload-service.js +2 -2
  25. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  26. package/dist/nodefony/src/context/Context.js +56 -13
  27. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  28. package/dist/nodefony/src/context/http/HttpContext.js +75 -25
  29. package/dist/nodefony/src/context/http/Request.js +15 -6
  30. package/dist/nodefony/src/context/http/Response.js +354 -35
  31. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  32. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  34. package/dist/nodefony/src/context/http2/Request.js +1 -1
  35. package/dist/nodefony/src/context/http2/Response.js +28 -5
  36. package/dist/nodefony/src/context/requestTrace.js +104 -0
  37. package/dist/nodefony/src/context/timingMode.js +21 -0
  38. package/dist/nodefony/src/context/transport.js +20 -1
  39. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  40. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
  42. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  43. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  44. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  45. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  46. package/dist/nodefony/src/cookies/cookie.js +54 -13
  47. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  48. package/dist/nodefony/src/profiler/Profiler.js +25 -22
  49. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  50. package/dist/nodefony/src/proxy/forward.js +109 -9
  51. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  52. package/dist/nodefony/src/proxy/rules.js +29 -14
  53. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  54. package/dist/types/index.d.ts +8 -0
  55. package/dist/types/nodefony/config/config.d.ts +8 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  57. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  58. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  61. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  62. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  64. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  65. package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
  66. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  68. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  69. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  70. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  71. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
  72. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  73. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  74. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  75. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  76. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  79. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  80. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  81. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
  83. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  84. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  85. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  86. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  87. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  88. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  89. package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
  90. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  91. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  92. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  93. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  94. package/docs/byte-stream.md +371 -0
  95. package/docs/cookies.md +13 -3
  96. package/docs/diagnostics-channel.md +303 -0
  97. package/docs/index.md +14 -1
  98. package/docs/observabilite.md +94 -16
  99. package/docs/reverse-proxy.md +33 -19
  100. package/docs/servers.md +26 -15
  101. package/docs/session.md +3 -3
  102. package/docs/sse.md +27 -20
  103. package/package.json +16 -16
@@ -20,106 +20,12 @@
20
20
  */
21
21
  import type { ProfiledResolver } from "./FrameProfile.js";
22
22
  import type { ProtocolName } from "../../interfaces/IContext.js";
23
- /** Une phase du pipeline, projetée pour le transport (durée résolue). */
24
- export interface ProfilePhase {
25
- name: string;
26
- /** Début relatif `performance.now()` (ms) — sert au calcul du waterfall. */
27
- startMs: number;
28
- /** Durée en ms, `null` si la phase ne s'est pas terminée (erreur). */
29
- durationMs: number | null;
30
- }
31
- /**
32
- * Une requête SQL/NoSQL exécutée pendant la requête HTTP (SEAM ORM — futur).
33
- *
34
- * Non collecté aujourd'hui : quand `@nodefony/orm-core` arrivera, les adapters
35
- * pousseront leurs requêtes dans un buffer per-request dev-only (via l'ALS
36
- * `RequestContext.getRequestId()`, déjà en place) que `collect()` lira ici.
37
- * Le champ `queries` reste donc `undefined` tant qu'aucun ORM ne pushe → 0 coût.
38
- */
39
- export interface ProfileQuery {
40
- /** Requête (SQL ou commande NoSQL), tronquée si volumineuse. */
41
- sql: string;
42
- /** Début relatif (`performance.now()`) — même horloge que les phases. */
43
- startMs?: number | undefined;
44
- /** Durée d'exécution en ms. */
45
- durationMs: number;
46
- /** Lignes affectées/retournées, si connu. */
47
- rows?: number | undefined;
48
- /** Connecteur émetteur (`drizzle`, `mongoose`…). */
49
- connector?: string | undefined;
50
- }
51
- /**
52
- * Traversée du firewall par cette requête — **ce qui était possible** (la zone :
53
- * son nom, si elle est protégée, quels authenticators elle accepte) croisé avec
54
- * **ce qui s'est passé** (quel maillon a résolu l'identité, l'issue, le motif
55
- * d'un refus).
56
- *
57
- * Sans cela, une requête qui PASSE est invisible côté sécurité : le chemin de
58
- * succès n'émet aucun événement d'audit (choix délibéré — le volume nominal
59
- * n'est pas un signal). `undefined` hors zone firewall.
60
- */
61
- export interface ProfileSecurity {
62
- /** Nom de la zone traversée. */
63
- zone: string | null;
64
- /** La zone exige-t-elle une identité (`security: true`) ? */
65
- protected: boolean;
66
- /** Chaîne d'authenticators : `first` = le premier qui supporte, `all` = MFA. */
67
- mode: string | null;
68
- /** Authenticators que la zone accepte (ce qui était POSSIBLE). */
69
- candidates: string[];
70
- /** Authenticator qui a RÉELLEMENT résolu l'identité. */
71
- authenticator: string | null;
72
- /** `granted` · `anonymous` · `denied` · `failure` · `throttled` · `bypass` · `public`. */
73
- outcome: string | null;
74
- /** Motif du refus (`no_credentials`, `invalid_credentials`…). */
75
- reason: string | null;
76
- /** Rôles du token résolu (l'axe autorisation). */
77
- roles: string[] | null;
78
- }
79
- /** Profil complet d'une requête (HTTP ou message WS), exposé par `get`. */
80
- export interface ProfileEntry {
81
- requestId: string;
82
- /**
83
- * Numéro d'ordre de collecte, croissant sur la vie du profileur. Un profil
84
- * réinséré (identifiant réutilisé) reçoit un numéro neuf.
85
- */
86
- seq: number;
87
- /** Horodatage de la collecte (`Date.now()`), pour le tri client. */
88
- ts: number;
89
- kind: "http" | "ws";
90
- method: string | null;
91
- url: string;
92
- scheme: string;
93
- status: number | null;
94
- /** Durée totale serveur (ms), dérivée des phases. */
95
- durationMs: number | null;
96
- route: string | null;
97
- controller: string | null;
98
- action: string | null;
99
- remoteAddress: string | null;
100
- /** Identité (username) si le firewall l'a injectée, sinon `null`. */
101
- user: string | null;
102
- /** `traceparent` W3C Trace Context (P2.7) — corrélation distribuée RFC-propre. */
103
- traceparent: string | null;
104
- error: string | null;
105
- phases: ProfilePhase[];
106
- /** Requêtes ORM (SEAM futur — `undefined` tant qu'aucun adapter ne pushe). */
107
- queries?: ProfileQuery[] | undefined;
108
- /** Traversée du firewall — `undefined` si la requête n'a croisé aucune zone. */
109
- security?: ProfileSecurity | undefined;
110
- }
111
- /** Résumé léger d'un profil pour la liste `recent` (sans les phases). */
112
- export interface ProfileSummary {
113
- requestId: string;
114
- ts: number;
115
- kind: "http" | "ws";
116
- method: string | null;
117
- url: string;
118
- status: number | null;
119
- durationMs: number | null;
120
- route: string | null;
121
- error: string | null;
122
- }
23
+ import type { IProfileEntry, IProfilePhase, IProfileQuery, IProfileSecurity, IProfileSummary } from "nodefony";
24
+ export type ProfilePhase = IProfilePhase;
25
+ export type ProfileQuery = IProfileQuery;
26
+ export type ProfileSecurity = IProfileSecurity;
27
+ export type ProfileEntry = IProfileEntry;
28
+ export type ProfileSummary = IProfileSummary;
123
29
  /** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
124
30
  interface ProfilableContext {
125
31
  requestId?: string | undefined;
@@ -142,6 +48,11 @@ interface ProfilableContext {
142
48
  durationMs?: number | undefined;
143
49
  }> | undefined;
144
50
  resolver?: ProfiledResolver | null | undefined;
51
+ /**
52
+ * Arrête l'horloge de la requête et rend sa durée (ms) — la même que le
53
+ * journal d'accès (`IContext.stopClock`). `null` si `timing.enabled` vaut `false`.
54
+ */
55
+ stopClock(): number | null;
145
56
  /**
146
57
  * Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
147
58
  * que la payload ALS — les adapters ORM y poussent via `RequestContext`.
@@ -205,6 +116,14 @@ export declare class Profiler implements IProfilerReader {
205
116
  private readonly _caps;
206
117
  /** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
207
118
  private _seq;
119
+ /**
120
+ * Démarrage du profileur, donc du processus (ms epoch, horloge du serveur) :
121
+ * aucune requête servie avant n'est connue. Rendu avec un profil absent, pour
122
+ * que le lecteur dise « le serveur a redémarré » plutôt que « évincé ».
123
+ */
124
+ readonly startedAt: number;
125
+ /** Dernier vidage des anneaux (ms epoch), `null` s'il n'y en a jamais eu. */
126
+ private _clearedAt;
208
127
  /**
209
128
  * @param cap - capacité de l'anneau de l'application.
210
129
  * @param sideCap - capacité de chacun des anneaux de l'outillage et des
@@ -222,8 +141,12 @@ export declare class Profiler implements IProfilerReader {
222
141
  get(requestId: string): ProfileEntry | undefined;
223
142
  /**
224
143
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
144
+ *
145
+ * @param limit - nombre maximal de résumés
146
+ * @param connection - le requestId d'une connexion WebSocket : ses SEULES
147
+ * frames du pont (`<connection>.<n>`, règle `frameIndexOf` du cœur)
225
148
  */
226
- recent(limit?: number): ProfileSummary[];
149
+ recent(limit?: number, connection?: string): ProfileSummary[];
227
150
  /** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
228
151
  get cursor(): number;
229
152
  /**
@@ -240,7 +163,9 @@ export declare class Profiler implements IProfilerReader {
240
163
  since(cursor: number): IProfileBatch;
241
164
  /** Nombre de profils en mémoire. */
242
165
  get size(): number;
243
- /** Vide les anneaux. */
166
+ /** Vide les anneaux, et retient l'instant du vidage ({@link clearedAt}). */
244
167
  clear(): void;
168
+ /** Dernier vidage des anneaux (ms epoch, horloge du serveur), `null` si aucun. */
169
+ get clearedAt(): number | null;
245
170
  }
246
171
  export default Profiler;
@@ -1,4 +1,6 @@
1
1
  import type { ProfileEntry } from "./Profiler.js";
2
+ import type { IStatusBreakdown, IPhaseTotal, IRouteStats, INPlusOne, ISqlShapeStats, IErrorSample, IProfileReport } from "nodefony";
3
+ export type { IStatusBreakdown, IPhaseTotal, IRouteStats, INPlusOne, ISqlShapeStats, IErrorSample, IProfileReport, };
2
4
  /**
3
5
  * Agrégation des profils de requêtes — règle PURE, sans horloge ni I/O.
4
6
  *
@@ -21,7 +23,14 @@ export declare const ROUTE_SAMPLE_CAP = 256;
21
23
  export declare const SQL_SHAPE_CAP = 200;
22
24
  /** Exemples d'erreurs retenus (statut ≥ 500 ou exception). */
23
25
  export declare const ERROR_SAMPLE_CAP = 10;
24
- /** Préfixe des routes de l'outillage (console, API d'administration, porte MCP). */
26
+ /**
27
+ * Préfixe de l'espace réservé au framework.
28
+ *
29
+ * @deprecated Ne dit pas ce qui est de l'outillage : une connexion
30
+ * d'utilisateur (`/nodefony/security/api/auth/…`) ou une route d'application
31
+ * (`/nodefony/test/index`) passent sous ce préfixe. La règle est
32
+ * `isConsolePath` (`nodefony`), que `profileLane` appelle.
33
+ */
25
34
  export declare const TOOLING_URL_PREFIX = "/nodefony/";
26
35
  /**
27
36
  * Provenance d'un profil : l'application, l'outillage (`/nodefony/…` :
@@ -30,8 +39,10 @@ export declare const TOOLING_URL_PREFIX = "/nodefony/";
30
39
  */
31
40
  export type ProfileLane = "app" | "tooling" | "asset";
32
41
  /**
33
- * Range un profil par provenance, d'après son URL. Seule règle du partage —
34
- * les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
42
+ * Range un profil par provenance, d'après son URL. L'outillage est reconnu par
43
+ * la règle du cœur, `isConsolePath` — la même que les Journaux, la barre de
44
+ * débogage et le Studio : une connexion d'utilisateur reste de l'application.
45
+ * Les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
35
46
  *
36
47
  * @param url - URL du profil, absolue ou chemin.
37
48
  * @returns la provenance du profil.
@@ -44,113 +55,19 @@ export declare function profileLane(url: string): ProfileLane;
44
55
  * @param url - URL du profil, absolue ou chemin.
45
56
  */
46
57
  export declare function isToolingUrl(url: string): boolean;
58
+ /**
59
+ * Une requête de cette URL se profile-t-elle ? L'application toujours ;
60
+ * l'outillage et les assets relayés seulement sur demande (`timing.tooling`)
61
+ * — on ne mesure pas la façon dont on mesure.
62
+ *
63
+ * @param url - URL ou chemin de la requête (ou de la frame du pont)
64
+ * @param tooling - profiler aussi l'outillage
65
+ */
66
+ export declare function isProfiledUrl(url: string, tooling: boolean): boolean;
47
67
  /** Groupe des requêtes qu'aucune route n'a servies (404, statique, sondes). */
48
68
  export declare const NO_ROUTE = "(aucune route)";
49
69
  /** Pseudo-phase : le temps de l'action hors phases imbriquées et hors SQL. */
50
70
  export declare const ACTION_SELF_PHASE = "action.self";
51
- /** Répartition des statuts HTTP d'une route. */
52
- export interface IStatusBreakdown {
53
- s2xx: number;
54
- s3xx: number;
55
- s4xx: number;
56
- s5xx: number;
57
- /** Dont 401 : la route exige une authentification que la requête n'avait pas. */
58
- unauthorized: number;
59
- /** Dont 403 : authentifiée, mais refusée. */
60
- forbidden: number;
61
- }
62
- /** Temps cumulé d'une phase sur les requêtes d'une route. */
63
- export interface IPhaseTotal {
64
- totalMs: number;
65
- /** Requêtes où la phase est apparue et s'est terminée. */
66
- count: number;
67
- }
68
- /** Statistiques d'une route sur la fenêtre agrégée. */
69
- export interface IRouteStats {
70
- route: string;
71
- kind: "http" | "ws";
72
- controller: string | null;
73
- action: string | null;
74
- count: number;
75
- /** Requêtes sans durée mesurée (mesure des phases désactivée, erreur précoce). */
76
- untimed: number;
77
- /**
78
- * Requêtes SERVIES et chronométrées (statut < 400, sans exception) : la
79
- * population de tout ce qui se dit « par requête » ci-dessous. Refus et
80
- * erreurs se lisent dans `status` et `errors`.
81
- */
82
- served: number;
83
- /** Percentiles et maximum, sur les requêtes servies. */
84
- p50Ms: number | null;
85
- p95Ms: number | null;
86
- maxMs: number | null;
87
- status: IStatusBreakdown;
88
- /** Requêtes en erreur : statut ≥ 500 ou exception levée. */
89
- errors: number;
90
- /**
91
- * Requêtes SQL/NoSQL des requêtes servies : total, maximum dans une
92
- * requête, temps CUMULÉ (des requêtes parallèles s'y additionnent).
93
- */
94
- queries: {
95
- total: number;
96
- perRequestMax: number;
97
- totalMs: number;
98
- };
99
- /** Temps par phase des requêtes servies, `action.self` compris. */
100
- phases: Record<string, IPhaseTotal>;
101
- }
102
- /** Un N+1 : la même forme SQL répétée dans une requête. */
103
- export interface INPlusOne {
104
- route: string;
105
- controller: string | null;
106
- action: string | null;
107
- shape: string;
108
- /** Plus grand nombre de répétitions vu dans une requête. */
109
- maxRepeats: number;
110
- /** Requêtes touchées. */
111
- requests: number;
112
- /** Temps cumulé de ces répétitions. */
113
- totalMs: number;
114
- /** Une requête touchée, pour aller voir son profil complet. */
115
- sampleRequestId: string;
116
- }
117
- /** Une forme SQL et son coût sur la fenêtre. */
118
- export interface ISqlShapeStats {
119
- shape: string;
120
- connector: string | null;
121
- count: number;
122
- totalMs: number;
123
- maxMs: number;
124
- /** Routes qui l'ont émise (au plus cinq). */
125
- routes: string[];
126
- }
127
- /** Un exemple d'erreur, de quoi la retrouver dans le journal. */
128
- export interface IErrorSample {
129
- route: string;
130
- status: number | null;
131
- error: string | null;
132
- requestId: string;
133
- }
134
- /** Le rapport agrégé. */
135
- export interface IProfileReport {
136
- /** Requêtes agrégées (hors outillage filtré). */
137
- requests: number;
138
- http: number;
139
- ws: number;
140
- /** Requêtes de l'outillage écartées (`/nodefony/…`, `/_vite/…`). */
141
- filteredTooling: number;
142
- /** Requêtes sans durée mesurée. */
143
- untimed: number;
144
- /** Triées par p95 décroissant. */
145
- routes: IRouteStats[];
146
- /** Triés par temps cumulé décroissant. */
147
- nPlusOne: INPlusOne[];
148
- /** Les dix formes SQL les plus coûteuses (temps cumulé décroissant). */
149
- slowQueries: ISqlShapeStats[];
150
- /** Formes SQL distinctes vues au-delà de la capacité, comptées sans détail. */
151
- sqlShapesDropped: number;
152
- errors: IErrorSample[];
153
- }
154
71
  /** Réglages d'une agrégation. */
155
72
  export interface IProfileAggregatorOptions {
156
73
  /** Répétitions à partir desquelles une forme SQL est un N+1. */
@@ -55,6 +55,19 @@ export declare const MAX_VIA_HOPS = 10;
55
55
  * @param pseudonym - pseudonyme de ce processus
56
56
  */
57
57
  export declare function viaLoops(via: string | string[] | undefined, pseudonym: string): boolean;
58
+ /**
59
+ * `Max-Forwards` reçu, tel qu'un intermédiaire doit le lire (RFC 9110 §7.6.2) :
60
+ * sur `OPTIONS` et `TRACE` seulement — la même section permet de l'ignorer
61
+ * pour toute autre méthode, et il part alors tel quel.
62
+ *
63
+ * @param method - méthode de la requête
64
+ * @param value - en-tête `Max-Forwards` reçu
65
+ * @returns `null` s'il n'y a rien à faire (autre méthode, en-tête absent) ;
66
+ * `-1` si la valeur n'est pas `1*DIGIT` — elle ne se décrémente pas, l'en-tête
67
+ * n'est donc pas transmis ; sinon la valeur reçue (`0` : le proxy répond
68
+ * lui-même, sans joindre l'amont).
69
+ */
70
+ export declare function receivedMaxForwards(method: string, value: string | string[] | undefined): number | null;
58
71
  /**
59
72
  * Cadrage du corps transmis à l'amont (RFC 9112 §6). `unsupported` : un
60
73
  * codage de transfert autre que `chunked` seul — le proxy ne sait pas le
@@ -118,8 +131,9 @@ export declare function upstreamPath(url: string, mount: IProxyMount): string;
118
131
  * Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
119
132
  *
120
133
  * Le corps est transmis avec un cadrage cohérent ({@link bodyFramingOf}). Un
121
- * message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Un
122
- * client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
134
+ * message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Sur
135
+ * `OPTIONS` et `TRACE`, `Max-Forwards` est décrémenté ; à `0`, le proxy répond
136
+ * lui-même en destinataire final (RFC 9110 §7.6.2). Un client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
123
137
  * connexion non établie à temps ou un amont muet rendent 504 — à condition
124
138
  * que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
125
139
  * statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
@@ -154,6 +168,20 @@ export declare function rawHead(status: string, rawHeaders: readonly string[], s
154
168
  * @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`…)
155
169
  */
156
170
  export declare function refuseUpgrade(socket: Duplex, status: number, reason: string, headers?: Readonly<Record<string, string | number>> | null): void;
171
+ /**
172
+ * Refuse un handshake WebSocket non conforme — la réponse qui convient à la
173
+ * raison rendue par `websocketHandshakeProblem`, seule implémentation pour le
174
+ * serveur WebSocket ET le proxy inverse.
175
+ *
176
+ * Une version inconnue reçoit `426` avec `Sec-WebSocket-Version: 13` : le
177
+ * client apprend vers quelle version se rabattre (RFC 6455 §4.2.2 point 6,
178
+ * §4.4), et le 426 porte l'`Upgrade` qu'exige RFC 9110 §15.5.22. Tout autre
179
+ * défaut de forme reçoit `400`.
180
+ *
181
+ * @param socket - socket du client, encore en attente de sa réponse
182
+ * @param problem - la raison rendue par `websocketHandshakeProblem`
183
+ */
184
+ export declare function refuseMalformedHandshake(socket: Duplex, problem: string): void;
157
185
  /**
158
186
  * Relaie un upgrade WebSocket DÉJÀ validé (méthode, `Upgrade`, clé, version,
159
187
  * `Origin` — cf `ReverseProxy.handleUpgrade`) : rejoue le handshake vers
@@ -73,6 +73,26 @@ export declare function proxyMountProblems(prefix: string, mount: IProxyMountSha
73
73
  * @returns `true` si le chemin doit être refusé
74
74
  */
75
75
  export declare function isAmbiguousPath(url: string): boolean;
76
+ /**
77
+ * Le corps reçu porte-t-il un codage de transfert que le serveur ne sait pas
78
+ * défaire ? Seul `chunked` est compris : Node le retire, mais laisse en place
79
+ * tout codage qui le précède (`gzip, chunked`) — le corps livré serait encore
80
+ * compressé. RFC 9112 §6.1 : un tel message reçoit 501. Un `chunked` qui n'est
81
+ * pas le DERNIER codage est déjà refusé par Node en 400 (§6.3).
82
+ *
83
+ * Seule règle, pour le serveur d'origine et pour le proxy inverse. Chemin de
84
+ * chaque requête : une lecture d'en-tête, rien d'autre sans `Transfer-Encoding`.
85
+ *
86
+ * @param coding - valeur de l'en-tête `Transfer-Encoding`, absente le plus souvent
87
+ * @returns `true` quand le corps ne peut pas être lu tel qu'annoncé
88
+ */
89
+ export declare function isUnsupportedTransferCoding(coding: string | undefined): boolean;
90
+ /**
91
+ * Raison rendue par {@link websocketHandshakeProblem} quand le client parle une
92
+ * autre version du protocole : la seule qui appelle une réponse distincte —
93
+ * le client doit apprendre vers quelle version se rabattre (RFC 6455 §4.4).
94
+ */
95
+ export declare const WS_UNSUPPORTED_VERSION = "Sec-WebSocket-Version \u2260 13";
76
96
  /**
77
97
  * Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
78
98
  * un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
@@ -1,6 +1,6 @@
1
1
  import { type IncomingMessage } from "node:http";
2
2
  import type { Duplex } from "node:stream";
3
- import type { WebSocket, WebSocketServer } from "ws";
3
+ import { WebSocketServer, type ServerOptions, type WebSocket } from "ws";
4
4
  /** Serveur qui émet `upgrade` : HTTP/1.1, HTTPS ou HTTP/2 sécurisé (`allowHTTP1`). */
5
5
  interface IUpgradeEmitter {
6
6
  on(event: "upgrade", listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): unknown;
@@ -26,6 +26,18 @@ export interface IUpgradeAdmission {
26
26
  * le handshake) — il n'y a alors rien à servir
27
27
  */
28
28
  accept(): WebSocket | null | Promise<WebSocket | null>;
29
+ /**
30
+ * Retient le sous-protocole que le serveur renverra dans le `101` (RFC 6455
31
+ * §4.2.2 : « the subprotocol the server is ready to use »), à appeler AVANT
32
+ * {@link accept}. Sans choix, la règle d'avant s'applique : l'option
33
+ * `handleProtocols` de la configuration `ws`, sinon le premier offert.
34
+ *
35
+ * Un choix absent de l'offre du client n'est jamais renvoyé — la norme ne
36
+ * permet que l'un des sous-protocoles offerts, ou aucun.
37
+ *
38
+ * @param protocol - le sous-protocole choisi, `false` pour n'en renvoyer aucun
39
+ */
40
+ selectSubprotocol(protocol: string | false): void;
29
41
  /**
30
42
  * Refuse par un statut HTTP, AVANT le `101` (RFC 6455 §4.2.2).
31
43
  *
@@ -36,6 +48,34 @@ export interface IUpgradeAdmission {
36
48
  }
37
49
  /** Ce qui décide d'un upgrade : reçoit la requête et rend son verdict, plus tard s'il le faut. */
38
50
  export type UpgradeAdmit = (req: IncomingMessage, admission: IUpgradeAdmission) => void;
51
+ /** Signature de l'option `handleProtocols` de `ws` (`ServerOptions`). */
52
+ export type SubprotocolHandler = (protocols: Set<string>, request: IncomingMessage) => string | false;
53
+ /**
54
+ * Fabrique l'option `handleProtocols` du serveur `ws` de Nodefony : le choix
55
+ * retenu par l'admission ({@link IUpgradeAdmission.selectSubprotocol}) s'il y
56
+ * en a un, sinon `fallback` (celle de la configuration), sinon le premier
57
+ * offert — le défaut de `ws` 8, conservé pour tout contrôleur qui ne choisit
58
+ * pas.
59
+ *
60
+ * @param fallback - l'option `handleProtocols` de la configuration, s'il y en a une
61
+ * @returns le rappel à passer à `new WebSocketServer({ handleProtocols })`
62
+ */
63
+ export declare function createSubprotocolHandler(fallback?: SubprotocolHandler): SubprotocolHandler;
64
+ /**
65
+ * Construit le serveur `ws` de Nodefony à partir des options de la
66
+ * configuration, en forçant ce que Nodefony gère — seul endroit où ces
67
+ * options sont posées, pour le serveur clair comme pour le sécurisé :
68
+ *
69
+ * - `noServer` : l'upgrade passe d'abord par le proxy inverse, puis par
70
+ * {@link attachUpgradeDispatch} ;
71
+ * - `clientTracking` : requis par `broadcast()` et le heartbeat ;
72
+ * - `handleProtocols` : le sous-protocole CHOISI par le contrôleur
73
+ * ({@link createSubprotocolHandler}), celui de la configuration à défaut.
74
+ *
75
+ * @param options - options `ws` issues de la configuration du serveur WebSocket
76
+ * @returns le serveur, sans écoute propre
77
+ */
78
+ export declare function createNodefonyWebSocketServer(options: ServerOptions): WebSocketServer;
39
79
  /**
40
80
  * Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
41
81
  * puis le serveur WebSocket de Nodefony.