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

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 (96) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -2
  4. package/dist/nodefony/config/config.js +25 -2
  5. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  6. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  7. package/dist/nodefony/grpc/index.js +4 -0
  8. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  9. package/dist/nodefony/service/audit-logger.js +52 -6
  10. package/dist/nodefony/service/error-renderer.js +41 -9
  11. package/dist/nodefony/service/http-kernel.js +198 -19
  12. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  13. package/dist/nodefony/service/request-logger.js +21 -6
  14. package/dist/nodefony/service/reverse-proxy.js +1 -1
  15. package/dist/nodefony/service/servers/clientError.js +26 -1
  16. package/dist/nodefony/service/servers/server-h2c.js +3 -3
  17. package/dist/nodefony/service/servers/server-http.js +3 -3
  18. package/dist/nodefony/service/servers/server-https.js +4 -4
  19. package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
  20. package/dist/nodefony/service/servers/server-websocket.js +3 -3
  21. package/dist/nodefony/service/servers/serverShutdown.js +14 -36
  22. package/dist/nodefony/service/sessions/sessions-service.js +6 -6
  23. package/dist/nodefony/service/upload/upload-service.js +2 -2
  24. package/dist/nodefony/src/context/Context.js +12 -1
  25. package/dist/nodefony/src/context/http/ByteStream.js +9 -4
  26. package/dist/nodefony/src/context/http/HttpContext.js +86 -8
  27. package/dist/nodefony/src/context/http/Request.js +96 -16
  28. package/dist/nodefony/src/context/http/Response.js +120 -17
  29. package/dist/nodefony/src/context/http/SseStream.js +4 -4
  30. package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
  31. package/dist/nodefony/src/context/http/parser.js +29 -11
  32. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  33. package/dist/nodefony/src/context/http2/Response.js +47 -8
  34. package/dist/nodefony/src/context/requestTrace.js +6 -2
  35. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  36. package/dist/nodefony/src/context/websocket/Response.js +11 -1
  37. package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
  38. package/dist/nodefony/src/cookies/cookie.js +3 -1
  39. package/dist/nodefony/src/errors/httpError.js +58 -25
  40. package/dist/nodefony/src/grpc/cors.js +59 -0
  41. package/dist/nodefony/src/grpc/limits.js +16 -0
  42. package/dist/nodefony/src/grpc/refusal.js +271 -0
  43. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  44. package/dist/nodefony/src/proxy/forward.js +1 -1
  45. package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
  46. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  47. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  48. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  49. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  50. package/dist/types/index.d.ts +5 -2
  51. package/dist/types/nodefony/config/config.d.ts +10 -0
  52. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  53. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  54. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  55. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  56. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  57. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  58. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  60. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  61. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  62. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  64. package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
  65. package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
  66. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
  68. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  69. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
  71. package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
  72. package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
  73. package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
  74. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  76. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  79. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  80. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  81. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
  82. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  83. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  84. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  85. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  86. package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
  87. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  88. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  89. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  90. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  91. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  92. package/docs/byte-stream.md +52 -17
  93. package/docs/diagnostics-channel.md +45 -1
  94. package/docs/servers.md +57 -44
  95. package/docs/upload.md +43 -17
  96. package/package.json +26 -6
@@ -0,0 +1,100 @@
1
+ import http2 from "node:http2";
2
+ // nodefony/src/servers/http2StreamGrace.ts
3
+ /**
4
+ * Délai avant de retenter un `PING` que Node a refusé (plafond de `PING` en vol
5
+ * atteint par d'autres émetteurs de la session) : ceux-là reviennent en un
6
+ * aller-retour, une boucle `setImmediate` brûlerait le processeur pendant ce
7
+ * temps sur un lien lent.
8
+ */
9
+ const PING_RETRY_MS = 10;
10
+ /**
11
+ * Les flux d'une session qui attendent leur grâce. `inFlight` : couverts par le
12
+ * `PING` en vol ; `next` : arrivés après son envoi, donc couverts par le
13
+ * suivant — un `PING` envoyé AVANT leurs en-têtes ne prouve rien pour eux.
14
+ */
15
+ var SessionGrace = class {
16
+ inFlight = null;
17
+ next = null;
18
+ };
19
+ /** Chemin d'erreur seulement : aucune session n'a d'entrée hors refus en cours. */
20
+ const graces = /* @__PURE__ */ new WeakMap();
21
+ /** Coupe le flux en `NO_ERROR` s'il est encore ouvert et que le client émet encore. */
22
+ function decide(stream) {
23
+ if (stream.closed || stream.destroyed || stream.state.remoteClose) return;
24
+ stream.close(http2.constants.NGHTTP2_NO_ERROR);
25
+ }
26
+ function sendPing(session, grace) {
27
+ if (session.destroyed) {
28
+ graces.delete(session);
29
+ return;
30
+ }
31
+ if (session.closed) {
32
+ const pending = grace.inFlight;
33
+ const later = grace.next;
34
+ grace.inFlight = null;
35
+ grace.next = null;
36
+ graces.delete(session);
37
+ setImmediate(() => {
38
+ if (pending !== null) for (const stream of pending) decide(stream);
39
+ if (later !== null) for (const stream of later) decide(stream);
40
+ });
41
+ return;
42
+ }
43
+ if (!session.ping(() => settle(session, grace))) setTimeout(() => sendPing(session, grace), PING_RETRY_MS).unref();
44
+ }
45
+ function settle(session, grace) {
46
+ const batch = grace.inFlight;
47
+ grace.inFlight = grace.next;
48
+ grace.next = null;
49
+ if (batch !== null) for (const stream of batch) decide(stream);
50
+ if (grace.inFlight !== null) sendPing(session, grace);
51
+ else graces.delete(session);
52
+ }
53
+ /**
54
+ * Ferme en `NO_ERROR` un flux HTTP/2 dont la réponse est partie mais dont le
55
+ * client émet encore, après la grâce d'un aller-retour (`PING`, RFC 9113 §6.7).
56
+ *
57
+ * RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
58
+ * client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. La coupe ne doit
59
+ * jamais précéder l'envoi de la réponse : un `RST_STREAM` soumis pendant que
60
+ * les en-têtes attendent encore dans la file de nghttp2 les fait JETER, et le
61
+ * client reçoit une fin de flux sans statut (un client gRPC la lit INTERNAL).
62
+ * Le `PING` part derrière la réponse ; son écho prouve qu'elle est arrivée.
63
+ *
64
+ * **Un seul `PING` par session** couvre tous les flux qui attendent : un flux
65
+ * arrivé pendant qu'un `PING` est en vol attend le suivant. Le plafond de
66
+ * `PING` en vol de Node (10 par session) n'est donc jamais atteint par ce
67
+ * chemin, et aucun flux n'est coupé sans grâce — ce que faisait la coupe
68
+ * immédiate quand `ping()` rendait `false`, au-delà de 10 refus simultanés.
69
+ *
70
+ * Session en fermeture GRACIEUSE (délai d'inactivité, arrêt) : Node n'envoie
71
+ * plus de `PING`, et la session attend la fin de ses flux — un client muet la
72
+ * retiendrait. La coupe part alors au tour de boucle suivant, le temps que les
73
+ * réponses déjà soumises quittent la file de nghttp2.
74
+ *
75
+ * Après la grâce : client qui a fini d'émettre → rien (le flux se ferme seul,
76
+ * sans `RST_STREAM`) ; sinon → coupé. L'écho d'un `PING` passe avant les autres
77
+ * trames (§6.7) : une trame DATA encore en route peut arriver après lui, la
78
+ * coupe reste conforme (§8.1).
79
+ *
80
+ * Coût : chemin d'erreur seulement — une entrée de `WeakMap` par session tant
81
+ * que des flux attendent, un `PING` par aller-retour et par session.
82
+ *
83
+ * @param stream - flux dont la réponse est ÉCRITE (`finish` passé).
84
+ */
85
+ function closeStreamAfterGrace(stream) {
86
+ if (stream.closed || stream.destroyed || stream.state.remoteClose) return;
87
+ const session = stream.session;
88
+ if (session === void 0) return;
89
+ let grace = graces.get(session);
90
+ if (grace === void 0) {
91
+ grace = new SessionGrace();
92
+ graces.set(session, grace);
93
+ }
94
+ if (grace.inFlight === null) {
95
+ grace.inFlight = [stream];
96
+ sendPing(session, grace);
97
+ } else if (grace.next === null) grace.next = [stream];
98
+ else grace.next.push(stream);
99
+ }
100
+ export { closeStreamAfterGrace };
@@ -0,0 +1,177 @@
1
+ // nodefony/src/servers/receiveDeadline.ts
2
+ /** Capacité de départ de l'anneau, et celle où il revient une fois vide. */
3
+ const INITIAL_CAPACITY = 64;
4
+ /**
5
+ * Une échéance ABSOLUE pour des réceptions en cours, sans un minuteur par
6
+ * réception : un seul balayage périodique pour toutes.
7
+ *
8
+ * ## Pourquoi
9
+ *
10
+ * `node:http2` ne borne pas la réception d'une requête : il n'a que des délais
11
+ * d'INACTIVITÉ (`http2stream.setTimeout`, `http2session.setTimeout`), qu'un
12
+ * client tient en vie avec un octet avant chaque échéance. `requestTimeout`,
13
+ * la borne absolue anti « slow-loris », n'existe que pour HTTP/1.1.
14
+ *
15
+ * ## Le coût, et pourquoi cette forme
16
+ *
17
+ * C'est la forme que Node donne lui-même à `requestTimeout` : aucun minuteur
18
+ * par requête, un contrôle périodique (`connectionsCheckingInterval`). Les
19
+ * réceptions suivies vivent dans un ANNEAU — un tableau des éléments et un
20
+ * `Int32Array` des tours d'entrée, indexés par un numéro de séquence :
21
+ * - suivre = deux écritures ; libérer = une comparaison et une écriture. Ni
22
+ * minuteur, ni fermeture, ni lecture d'horloge (le temps se compte en TOURS),
23
+ * ni champ ajouté à l'élément suivi ;
24
+ * - mesuré contre une `Map` (hachage d'identité de la clé) : ~26–32 ns par
25
+ * réception contre ~90–150 ns, de 1 à 10 000 réceptions simultanées.
26
+ *
27
+ * Toutes les entrées ont le même délai : l'ordre d'insertion EST l'ordre
28
+ * d'échéance. Un tour avance la tête et s'arrête à la première entrée vivante
29
+ * non échue — il ne coûte que ce qu'il expire ou saute.
30
+ *
31
+ * Contrepartie, bornée : une case libérée DERRIÈRE une entrée vivante attend
32
+ * que la tête la dépasse, soit au plus un délai de réception — 12 octets par
33
+ * réception commencée dans cette fenêtre. Sous charge, l'anneau garde sa
34
+ * capacité d'un tour à l'autre (jamais rétréci puis recrû) ; il la rend au
35
+ * premier tour sans réception nouvelle. Le minuteur unique est `unref`, et s'arrête au
36
+ * même moment.
37
+ *
38
+ * ## Précision
39
+ *
40
+ * Une entrée expire entre `timeoutMs` et `timeoutMs + periodMs` après son
41
+ * suivi — jamais AVANT le délai configuré.
42
+ *
43
+ * @typeParam T - l'élément suivi (un contexte de requête)
44
+ */
45
+ var ReceiveDeadline = class {
46
+ #items = new Array(INITIAL_CAPACITY).fill(null);
47
+ #ticks = new Int32Array(INITIAL_CAPACITY);
48
+ #mask = 63;
49
+ /** Numéro de séquence de la plus ancienne case non balayée. */
50
+ #head = 0;
51
+ /** Numéro de séquence de la prochaine entrée. */
52
+ #tail = 0;
53
+ #tick = 0;
54
+ #timer = null;
55
+ #onExpire;
56
+ #periodMs;
57
+ /** Tours pleins qu'une entrée doit voir passer avant d'expirer. */
58
+ #ttl;
59
+ /**
60
+ * @param timeoutMs - le délai de réception (> 0)
61
+ * @param onExpire - appelé une fois par entrée échue, déjà retirée
62
+ * @param periodMs - la période du balayage, bornée par le délai (défaut 1 s)
63
+ * @throws RangeError si `timeoutMs` ou `periodMs` n'est pas un entier > 0
64
+ */
65
+ constructor(timeoutMs, onExpire, periodMs = 1e3) {
66
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new RangeError(`délai de réception invalide : ${timeoutMs}`);
67
+ if (!Number.isInteger(periodMs) || periodMs <= 0) throw new RangeError(`période de balayage invalide : ${periodMs}`);
68
+ this.#onExpire = onExpire;
69
+ this.#periodMs = Math.min(periodMs, timeoutMs);
70
+ this.#ttl = Math.ceil(timeoutMs / this.#periodMs);
71
+ }
72
+ /** Cases occupées, libérées ou non, entre la tête et la queue. */
73
+ get size() {
74
+ return this.#tail - this.#head;
75
+ }
76
+ /** Capacité courante de l'anneau. */
77
+ get capacity() {
78
+ return this.#mask + 1;
79
+ }
80
+ /** Vrai tant que le balayage tourne. */
81
+ get running() {
82
+ return this.#timer !== null;
83
+ }
84
+ /**
85
+ * Suit une réception qui commence ; démarre le balayage s'il dormait.
86
+ *
87
+ * @param item - la réception
88
+ * @returns le numéro à rendre à {@link ReceiveDeadline.release}
89
+ */
90
+ track(item) {
91
+ if (this.#tail - this.#head > this.#mask) this.#grow();
92
+ const seq = this.#tail++;
93
+ const index = seq & this.#mask;
94
+ this.#items[index] = item;
95
+ this.#ticks[index] = this.#tick;
96
+ if (this.#timer === null) {
97
+ this.#timer = setInterval(() => this.#sweep(), this.#periodMs);
98
+ this.#timer.unref();
99
+ }
100
+ return seq;
101
+ }
102
+ /**
103
+ * Cesse de suivre une réception terminée (reçue, refusée ou échue). Sans
104
+ * effet si la case a déjà été balayée ou réattribuée : l'identité de
105
+ * l'élément est vérifiée. Le balayage ne s'arrête pas ici mais au tour
106
+ * suivant s'il ne reste rien — jamais un minuteur armé puis désarmé par
107
+ * requête.
108
+ *
109
+ * @param seq - le numéro rendu par {@link ReceiveDeadline.track}
110
+ * @param item - la réception suivie
111
+ */
112
+ release(seq, item) {
113
+ const index = seq & this.#mask;
114
+ if (this.#items[index] === item) this.#items[index] = null;
115
+ }
116
+ /** Arrête le balayage et oublie toutes les réceptions (arrêt du serveur). */
117
+ close() {
118
+ if (this.#timer !== null) {
119
+ clearInterval(this.#timer);
120
+ this.#timer = null;
121
+ }
122
+ this.#reset();
123
+ }
124
+ #sweep() {
125
+ const tick = ++this.#tick;
126
+ const items = this.#items;
127
+ const carried = this.#tail - this.#head;
128
+ while (this.#head < this.#tail) {
129
+ const index = this.#head & this.#mask;
130
+ const item = items[index] ?? null;
131
+ if (item !== null && tick - (this.#ticks[index] ?? 0) <= this.#ttl) break;
132
+ items[index] = null;
133
+ this.#head++;
134
+ if (item !== null) this.#onExpire(item);
135
+ }
136
+ if (this.#head !== this.#tail) return;
137
+ if (carried > 0) {
138
+ this.#head = 0;
139
+ this.#tail = 0;
140
+ return;
141
+ }
142
+ if (this.#timer !== null) {
143
+ clearInterval(this.#timer);
144
+ this.#timer = null;
145
+ }
146
+ this.#reset();
147
+ }
148
+ /**
149
+ * Anneau au repos ou fermé : numéros remis à zéro et capacité de départ
150
+ * rendue. Un numéro encore détenu par un appelant ne désigne plus son
151
+ * élément : `release` le reconnaît à l'identité.
152
+ */
153
+ #reset() {
154
+ this.#head = 0;
155
+ this.#tail = 0;
156
+ if (this.#mask + 1 !== INITIAL_CAPACITY) {
157
+ this.#items = new Array(INITIAL_CAPACITY).fill(null);
158
+ this.#ticks = new Int32Array(INITIAL_CAPACITY);
159
+ this.#mask = 63;
160
+ } else this.#items.fill(null);
161
+ }
162
+ /** Double la capacité, les entrées gardant leur numéro de séquence. */
163
+ #grow() {
164
+ const capacity = (this.#mask + 1) * 2;
165
+ const mask = capacity - 1;
166
+ const items = new Array(capacity).fill(null);
167
+ const ticks = new Int32Array(capacity);
168
+ for (let seq = this.#head; seq < this.#tail; seq++) {
169
+ items[seq & mask] = this.#items[seq & this.#mask] ?? null;
170
+ ticks[seq & mask] = this.#ticks[seq & this.#mask] ?? 0;
171
+ }
172
+ this.#items = items;
173
+ this.#ticks = ticks;
174
+ this.#mask = mask;
175
+ }
176
+ };
177
+ export { ReceiveDeadline };
@@ -77,7 +77,10 @@ export { decideSend, readBackpressureOptions, type WsBackpressurePolicy, type Ws
77
77
  export { admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter, FRAME_RATE_WINDOW_MS, type IFrameRateWindow, type IWsFrameRateOptions, } from "./nodefony/src/context/websocket/wsFrameRate.js";
78
78
  export { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
79
79
  export { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
80
+ export { rpcCorsLists } from "./nodefony/src/grpc/cors.js";
81
+ export type { IRpcCorsLists } from "./nodefony/src/grpc/cors.js";
80
82
  export type { IRequestTrace, RequestTraceProtocol, } from "./nodefony/interfaces/IRequestTrace.js";
83
+ export type { IRpcCall, IRpcOutcome, RpcSystem, } from "./nodefony/interfaces/IRpcCall.js";
81
84
  export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed, } from "./nodefony/src/context/domainMatcher.js";
82
85
  export type { DomainPattern, ITrustedHostsConfig, } from "./nodefony/src/context/domainMatcher.js";
83
86
  export { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
@@ -108,8 +111,8 @@ export type { IHttpResponse, IWebsocketResponse, } from "./nodefony/interfaces/I
108
111
  export type { IHttpKernel } from "./nodefony/interfaces/IHttpKernel.js";
109
112
  export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./nodefony/interfaces/IRouting.js";
110
113
  export type { ISecurityZone, IFirewallGate, } from "./nodefony/interfaces/ISecurity.js";
111
- export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, } from "./nodefony/interfaces/IUpload.js";
112
- export type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult, } from "./nodefony/interfaces/IErrorRenderer.js";
114
+ export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, IRouteUploadOptions, } from "./nodefony/interfaces/IUpload.js";
115
+ export type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult, IErrorDescription, } from "./nodefony/interfaces/IErrorRenderer.js";
113
116
  export type { IRequestLogger, IRequestLogEntry, } from "./nodefony/interfaces/IRequestLogger.js";
114
117
  export type { AuditLogEntry, AuditErrorEntry, JsonAuditLoggerOptions, } from "./nodefony/service/audit-logger.js";
115
118
  export type { ContextType, httpRequest, httpResponse, ProtocolType, } from "./nodefony/service/http-kernel.js";
@@ -108,6 +108,7 @@ export declare const httpConfigSchema: z.ZodObject<{
108
108
  }, z.core.$loose>]>>;
109
109
  skipUTF8Validation: z.ZodDefault<z.ZodBoolean>;
110
110
  autoPong: z.ZodDefault<z.ZodBoolean>;
111
+ maxPendingAutoPongs: z.ZodDefault<z.ZodNumber>;
111
112
  allowSynchronousEvents: z.ZodDefault<z.ZodBoolean>;
112
113
  maxBackpressure: z.ZodDefault<z.ZodNumber>;
113
114
  backpressureCloseAfterDrops: z.ZodDefault<z.ZodNumber>;
@@ -134,6 +135,7 @@ export declare const httpConfigSchema: z.ZodObject<{
134
135
  }, z.core.$loose>]>>;
135
136
  skipUTF8Validation: z.ZodDefault<z.ZodBoolean>;
136
137
  autoPong: z.ZodDefault<z.ZodBoolean>;
138
+ maxPendingAutoPongs: z.ZodDefault<z.ZodNumber>;
137
139
  allowSynchronousEvents: z.ZodDefault<z.ZodBoolean>;
138
140
  maxBackpressure: z.ZodDefault<z.ZodNumber>;
139
141
  backpressureCloseAfterDrops: z.ZodDefault<z.ZodNumber>;
@@ -208,6 +210,14 @@ export declare const httpConfigSchema: z.ZodObject<{
208
210
  }>>;
209
211
  }, z.core.$strict>>>;
210
212
  }, z.core.$strict>>;
213
+ grpc: z.ZodDefault<z.ZodObject<{
214
+ grpc: z.ZodDefault<z.ZodBoolean>;
215
+ grpcWeb: z.ZodDefault<z.ZodBoolean>;
216
+ connect: z.ZodDefault<z.ZodBoolean>;
217
+ readMaxBytes: z.ZodDefault<z.ZodNumber>;
218
+ writeMaxBytes: z.ZodDefault<z.ZodNumber>;
219
+ requireConnectProtocolHeader: z.ZodDefault<z.ZodBoolean>;
220
+ }, z.core.$strict>>;
211
221
  wsMaxConnectionsPerIp: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
212
222
  }, z.core.$strict>;
213
223
  /** Type de sortie (config normalisée + défauts appliqués). */
@@ -0,0 +1,142 @@
1
+ import type { ContextKey, ContextValues, HandlerContext } from "@connectrpc/connect";
2
+ import type { IRpcCall, RpcSystem } from "../interfaces/IRpcCall.js";
3
+ import type HttpContext from "../src/context/http/HttpContext.js";
4
+ /**
5
+ * Fiche d'un appel gRPC : ce que Nodefony attache à l'appel, et le
6
+ * `ContextValues` que Connect remet au gestionnaire (`ctx.values`).
7
+ *
8
+ * Connect alloue à chaque appel un `ContextValues` fait d'un objet littéral et
9
+ * de trois fermetures (`context-values.js#createContextValues`), dont la forme
10
+ * change à chaque `set`. La fiche le REMPLACE (option `contextValues` de
11
+ * l'adaptateur) : une classe à champs déclarés, méthodes au prototype, une
12
+ * allocation par appel au lieu de quatre.
13
+ *
14
+ * Les clés posées par un tiers (intercepteur, porte) sont confiées à un
15
+ * `createContextValues()` créé au premier `set` : aucune allocation tant que
16
+ * personne n'en pose, et aucune conversion de type — la valeur rendue est celle
17
+ * que le magasin de Connect a typée.
18
+ *
19
+ * C'est aussi l'appel RPC du contexte (`HttpContext.rpc`, ADR-0026 D10) : à
20
+ * l'échéance du délai de réponse, le noyau le termine par {@link expire} ; au
21
+ * `close` de la réponse, il en fixe le code par {@link settle}, que lisent la
22
+ * ligne d'accès, l'audit et le canal de traçage.
23
+ *
24
+ * @experimental Surface gRPC, ADR-0026 D11.
25
+ */
26
+ export declare class GrpcCall implements ContextValues, IRpcCall {
27
+ #private;
28
+ /**
29
+ * L'objet qui implémente le service pour CET appel : le contrôleur, une
30
+ * instance par appel sous `@Scope("request")`. `null` quand l'adaptateur est
31
+ * appelé hors de {@link GrpcEndpoint.serve} (jamais en service normal).
32
+ */
33
+ readonly target: object | null;
34
+ /**
35
+ * Le contexte de la requête qui porte l'appel : l'identité y est lue
36
+ * (`authenticatedUser()`) sur son magasin, jamais par l'ALS, pour choisir
37
+ * entre 16 et 7 (ADR-0026 D5, D7). `null` hors de {@link GrpcEndpoint.serve}.
38
+ */
39
+ readonly context: HttpContext | null;
40
+ constructor(target: object | null, context?: HttpContext | null);
41
+ /**
42
+ * Le protocole parlé sur le fil : celui du contexte d'appel (`grpc`,
43
+ * `grpc-web`, `connect`) ; à défaut, celui des en-têtes de la requête.
44
+ */
45
+ get system(): RpcSystem;
46
+ /**
47
+ * `paquet.Service/Méthode` : le chemin de la route, que l'adaptateur a
48
+ * reconnu à l'octet près. Lu à la première demande, puis retenu.
49
+ */
50
+ get method(): string;
51
+ /** Le code de l'appel (0 à 16), `null` avant {@link settle}. */
52
+ get code(): number | null;
53
+ /** Le nom du code (`OK`, `UNAVAILABLE`…), `null` avec lui. */
54
+ get codeName(): string | null;
55
+ /**
56
+ * Fixe le code de l'appel au `close` de la réponse (ADR-0026 D10 point 2) :
57
+ *
58
+ * - motif `ConnectError` → son code ; sa cause, si c'est une `Error`, part
59
+ * à la ligne d'accès (exception traduite par D7). Réponse NON finie : 1,
60
+ * sauf une fin posée par le serveur (échéance 4, interruption de la fiche)
61
+ * — le client est parti, quel que soit le code de son `RST_STREAM` ;
62
+ * - `AbortError` ou aucun motif → 0 si la réponse est finie, sinon 1 (le
63
+ * client est parti — le 499 du noyau, dit en gRPC) ;
64
+ * - autre valeur → 13, et elle part à la ligne d'accès.
65
+ *
66
+ * Une interruption du serveur (délai échu, identité révoquée) abaisse le
67
+ * signal avec sa `ConnectError` : elle suit la première règle. Sans contexte
68
+ * d'appel, le code reste `null`.
69
+ *
70
+ * @param ended - la réponse a-t-elle été terminée par le serveur ?
71
+ * @returns la cause à porter par la ligne d'accès, `null` sinon.
72
+ */
73
+ settle(ended: boolean): Error | null;
74
+ /**
75
+ * Lit une valeur posée par un tiers, ou la valeur par défaut de sa clé.
76
+ *
77
+ * @param key - la clé (`createContextKey`).
78
+ * @returns la valeur posée, sinon `key.defaultValue`.
79
+ */
80
+ get<T>(key: ContextKey<T>): T;
81
+ /**
82
+ * Pose une valeur pour la durée de l'appel.
83
+ *
84
+ * @param key - la clé (`createContextKey`).
85
+ * @param value - la valeur.
86
+ * @returns la fiche, pour chaîner.
87
+ */
88
+ set<T>(key: ContextKey<T>, value: T): this;
89
+ /**
90
+ * Retire une valeur ; sa clé rendra de nouveau sa valeur par défaut.
91
+ *
92
+ * @param key - la clé (`createContextKey`).
93
+ * @returns la fiche, pour chaîner.
94
+ */
95
+ delete(key: ContextKey<unknown>): this;
96
+ /**
97
+ * Le délai de réponse du serveur est échu : le signal de l'appel s'abaisse
98
+ * (`ctx.abort`) et l'appel se termine en 4 `DEADLINE_EXCEEDED`, même si le
99
+ * gestionnaire ne lit pas son signal. Échu avant que le gestionnaire ne
100
+ * démarre : il est terminé dès son démarrage. Sans effet après la fin.
101
+ */
102
+ expire(): void;
103
+ /**
104
+ * L'identité qui a ouvert l'appel n'est plus valable : le signal de l'appel
105
+ * s'abaisse et l'appel se termine en 16 `UNAUTHENTICATED`. Réponse unique :
106
+ * sa promesse est rejetée. Flux : il s'arrête au message suivant, même si le
107
+ * gestionnaire ne lit pas son signal — au plus le message déjà en vol part.
108
+ * Sans effet après la fin, ou après une autre interruption.
109
+ */
110
+ revoke(): void;
111
+ /**
112
+ * Le serveur s'arrête : le signal de l'appel s'abaisse et l'appel se termine
113
+ * en 14 `UNAVAILABLE` — le client le rejoue ailleurs (ADR-0026 D8). Flux : il
114
+ * s'arrête au message suivant, même si le gestionnaire ne lit pas son
115
+ * signal. Un gestionnaire bloqué qui n'écoute rien tombe avec sa connexion, à
116
+ * `shutdownTimeout`. Sans effet après la fin, ou après une autre interruption.
117
+ */
118
+ shutdown(): void;
119
+ }
120
+ /**
121
+ * Le contexte d'appel que Connect remet réellement au gestionnaire : son type
122
+ * public (`HandlerContext`) ne dit pas `abort`, que porte pourtant l'objet créé
123
+ * par `createHandlerContext` (`HandlerContextController`, non exporté).
124
+ *
125
+ * @experimental Surface gRPC, ADR-0026 D11.
126
+ */
127
+ export interface IAbortableHandlerContext extends HandlerContext {
128
+ /** Interrompt l'appel : `ctx.signal` s'abaisse avec `reason`. */
129
+ abort(reason?: unknown): void;
130
+ }
131
+ /**
132
+ * Garde de type : le contexte d'appel porte-t-il `abort` ? Connect le pose sur
133
+ * tout contexte qu'il crée ; le garde le CONSTATE au lieu de le supposer, pour
134
+ * qu'une version de Connect qui le retirerait se voie à l'exécution (et au
135
+ * test qui le garde) plutôt que par un appel qui ne s'interrompt plus.
136
+ *
137
+ * @param ctx - le contexte remis au gestionnaire.
138
+ * @returns `true` si `ctx.abort` est une fonction.
139
+ *
140
+ * @experimental Surface gRPC, ADR-0026 D11.
141
+ */
142
+ export declare function hasAbort(ctx: HandlerContext): ctx is IAbortableHandlerContext;
@@ -0,0 +1,98 @@
1
+ import type { DescService } from "@bufbuild/protobuf";
2
+ import type HttpContext from "../src/context/http/HttpContext.js";
3
+ /**
4
+ * Protocoles servis — la section `grpc` de la configuration http
5
+ * (ADR-0026 D3, point 5).
6
+ *
7
+ * @experimental Surface gRPC, ADR-0026 D11.
8
+ */
9
+ export interface IGrpcProtocols {
10
+ readonly grpc: boolean;
11
+ readonly grpcWeb: boolean;
12
+ readonly connect: boolean;
13
+ }
14
+ /**
15
+ * La section `grpc` de la configuration http : protocoles servis (D3, point
16
+ * 5) et bornes des messages (D4, points 1, 2 et 6).
17
+ *
18
+ * @experimental Surface gRPC, ADR-0026 D11.
19
+ */
20
+ export interface IGrpcSettings extends IGrpcProtocols {
21
+ /** Taille maximale d'un message reçu, après décompression (octets). */
22
+ readonly readMaxBytes: number;
23
+ /** Taille maximale d'un message émis (octets). */
24
+ readonly writeMaxBytes: number;
25
+ /** Exige l'en-tête `Connect-Protocol-Version` sur les requêtes Connect. */
26
+ readonly requireConnectProtocolHeader: boolean;
27
+ }
28
+ /**
29
+ * Options d'un {@link GrpcEndpoint}.
30
+ *
31
+ * @experimental Surface gRPC, ADR-0026 D11.
32
+ */
33
+ export interface IGrpcEndpointOptions {
34
+ /**
35
+ * `true` : la cible change à chaque appel (contrôleur `@Scope("request")`),
36
+ * chaque méthode la relit dans la fiche d'appel. `false` (défaut) : la cible
37
+ * du premier appel sert tous les suivants (contrôleur singleton) et ses
38
+ * méthodes sont liées une fois — aucune indirection par appel.
39
+ */
40
+ readonly perCall?: boolean;
41
+ }
42
+ /**
43
+ * Point d'entrée gRPC d'un service : possède l'adaptateur Connect
44
+ * (`connectNodeAdapter`) et lui confie la requête et la réponse BRUTES de Node
45
+ * d'un contexte déjà passé par le pipeline (pare-feu, gardes).
46
+ *
47
+ * L'adaptateur est construit au premier appel, depuis la section `grpc` de la
48
+ * configuration http. Il reçoit la fiche d'appel ({@link GrpcCall}) par son
49
+ * option `contextValues`, qu'il appelle de façon SYNCHRONE avant toute
50
+ * attente (`connect-node-adapter.js#nodeRequestHandler`) : la cible passe par
51
+ * une case lue à cet instant — ni ALS, ni promesse, ni écouteur de plus.
52
+ *
53
+ * Les méthodes s'inscrivent une à une par `router.rpc`, SANS options : des
54
+ * options passées à l'inscription rallumeraient gRPC et gRPC-Web coupés par la
55
+ * configuration (connectrpc/connect-es#1835). La restriction de protocoles
56
+ * vit sur l'adaptateur seul.
57
+ *
58
+ * @experimental Surface gRPC, ADR-0026 D11.
59
+ */
60
+ export declare class GrpcEndpoint<S extends DescService = DescService> {
61
+ #private;
62
+ /** Le contrat servi (descripteur généré par `buf`). */
63
+ readonly service: S;
64
+ /** Cible relue à chaque appel (`@Scope("request")`). */
65
+ readonly perCall: boolean;
66
+ constructor(service: S, options?: IGrpcEndpointOptions);
67
+ /**
68
+ * Remet la requête du contexte à l'adaptateur, qui écrit lui-même la
69
+ * réponse et ses trailers. Synchrone : rien n'est attendu ici.
70
+ *
71
+ * Avant de la remettre : le contexte est marqué répondu (`sended`), si bien
72
+ * que le pipeline n'attend aucune réponse de l'action.
73
+ *
74
+ * - Appel unaire : le délai de réponse de Nodefony reste armé, et termine
75
+ * l'appel en 4 `DEADLINE_EXCEEDED` par sa fiche (`HttpContext.rpc`,
76
+ * ADR-0026 D4 point 3).
77
+ * - Flux : aucune échéance. Il s'ouvre par le cycle de vie d'un flux
78
+ * (`openStreamLifecycle`, D4 point 4) — délai de réponse désarmé (à
79
+ * l'expiration il écrirait un 503 par-dessus l'adaptateur), place dans
80
+ * le budget de connexions longues par IP, client qui ne lit plus coupé
81
+ * par le balayage de blocage. Synchrone, sauf quand une session est à
82
+ * sauver : l'appel n'est remis qu'après.
83
+ *
84
+ * @param context - le contexte HTTP de la requête, pas encore répondu.
85
+ * @param target - l'objet qui implémente le service pour cet appel.
86
+ * @returns rien, ou la promesse de la remise quand une session est à sauver.
87
+ * @throws HttpError 500 quand la réponse est déjà partie, ou que son
88
+ * transport n'est pas reconnu.
89
+ * @throws HttpError 429 quand l'IP du client a déjà tous ses flux et sockets
90
+ * (`wsMaxConnectionsPerIp`) — rendu par le pipeline en 14 `UNAVAILABLE`.
91
+ * @throws HttpError 400 quand l'adaptateur refuse la requête avant d'avoir
92
+ * rien écrit (requête sans autorité `Host`) — la réponse revient alors au
93
+ * pipeline.
94
+ * @throws HttpError 404 quand aucune méthode ne correspond au chemin exact
95
+ * (casse modifiée) — rendu par le pipeline en 12 `UNIMPLEMENTED`.
96
+ */
97
+ serve(context: HttpContext, target: object): void | Promise<void>;
98
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@nodefony/http/grpc` — la primitive de transport gRPC (ADR-0026).
3
+ *
4
+ * Sous-chemin à part : le point d'entrée principal de `@nodefony/http` ne cite
5
+ * ni Connect ni protobuf, même en type (D12). Seule une application qui
6
+ * déclare un service l'importe — par `@nodefony/framework/grpc` — et charge
7
+ * alors Connect au démarrage.
8
+ *
9
+ * @experimental Surface gRPC, ADR-0026 D11.
10
+ */
11
+ export { GrpcCall, hasAbort } from "./GrpcCall.js";
12
+ export type { IAbortableHandlerContext } from "./GrpcCall.js";
13
+ export { GrpcEndpoint } from "./GrpcEndpoint.js";
14
+ export type { IGrpcEndpointOptions, IGrpcProtocols, IGrpcSettings, } from "./GrpcEndpoint.js";
15
+ export { markGrpcRoute } from "../src/grpc/refusal.js";
@@ -3,6 +3,7 @@ import type { MaybePromise } from "nodefony";
3
3
  import type { ICookie } from "./ICookie.js";
4
4
  import type { ISession } from "./ISession.js";
5
5
  import type { HTTPMethodType } from "./IRequest.js";
6
+ import type { IRpcCall } from "./IRpcCall.js";
6
7
  /**
7
8
  * Identité du serveur qui a reçu la requête, nommée par le schéma d'URI qu'il
8
9
  * sert en LOCAL (RFC 9110 §4.2, RFC 6455 §3) : elle choisit la section de
@@ -156,6 +157,14 @@ export interface IContext {
156
157
  export interface IHttpContext extends IContext {
157
158
  proxy: object | null;
158
159
  isRedirect: boolean;
160
+ /**
161
+ * L'appel RPC servi par cette requête (gRPC, gRPC-Web, Connect), `null` hors
162
+ * appel RPC — ADR-0026 D10. Son code est fixé au `close`, avant la ligne
163
+ * d'accès.
164
+ *
165
+ * @experimental Surface gRPC, ADR-0026 D11.
166
+ */
167
+ rpc: IRpcCall | null;
159
168
  /**
160
169
  * Sert la requête — synchrone quand l'action et son rendu le sont.
161
170
  *
@@ -11,6 +11,20 @@ export interface IErrorHttpResult {
11
11
  body: unknown;
12
12
  headers?: Record<string, string | number>;
13
13
  }
14
+ /**
15
+ * Ce qu'une route HTTP rendrait pour une erreur — son statut et le message
16
+ * destiné au client —, sans rien écrire. Sert les transports qui n'ont pas de
17
+ * corps d'erreur à rendre : un appel gRPC traduit ce statut en code
18
+ * (ADR-0026 D7).
19
+ *
20
+ * @experimental Surface gRPC, ADR-0026 D11.
21
+ */
22
+ export interface IErrorDescription {
23
+ /** Statut HTTP émettable (100 à 599). */
24
+ readonly status: number;
25
+ /** Message pour le client : opaque pour un 5xx en production. */
26
+ readonly message: string;
27
+ }
14
28
  /**
15
29
  * Aide jointe au corps d'erreur — ce que le développeur doit faire, en clair.
16
30
  *
@@ -1,5 +1,6 @@
1
1
  import type { URL } from "node:url";
2
2
  import type { ICookie } from "./ICookie.js";
3
+ import type { IUploadedFile } from "./IUpload.js";
3
4
  export type HTTPMethodType = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" | "WEBSOCKET";
4
5
  export interface IRequest {
5
6
  url: URL | string;
@@ -19,7 +20,7 @@ export interface IHttpRequest extends IRequest {
19
20
  url: URL;
20
21
  query: Record<string, unknown>;
21
22
  body: RequestBody;
22
- files: readonly unknown[];
23
+ files: readonly IUploadedFile[];
23
24
  remoteAddress: string | null | undefined;
24
25
  acceptHtml: boolean;
25
26
  getRemoteAddress(): string | null | undefined;
@@ -1,5 +1,6 @@
1
1
  import type { SchemeType } from "./IContext.js";
2
2
  import type { HTTPMethodType } from "./IRequest.js";
3
+ import type { IRpcOutcome } from "./IRpcCall.js";
3
4
  /**
4
5
  * Nature de l'opération tracée : une requête HTTP, ou le handshake d'une
5
6
  * connexion WebSocket.
@@ -53,4 +54,13 @@ export interface IRequestTrace {
53
54
  * sans exception n'en porte pas ; c'est à l'abonné de classer un statut.
54
55
  */
55
56
  readonly error: unknown;
57
+ /**
58
+ * L'appel RPC (gRPC, gRPC-Web, Connect) servi par la requête, `null` hors
59
+ * RPC : système, méthode, code et son nom (ADR-0026 D10 point 5). Un flux
60
+ * est UN message, de l'ouverture au `close` ; rien par message.
61
+ * `statusCode` reste le statut HTTP, vrai sur le fil.
62
+ *
63
+ * @experimental Surface gRPC, ADR-0026 D11.
64
+ */
65
+ readonly rpc: IRpcOutcome | null;
56
66
  }