@nodefony/http 10.0.0-beta.1 → 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.
- package/README.md +5 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +48 -7
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +214 -158
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +9 -4
- package/dist/nodefony/src/context/http/Response.js +46 -7
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +17 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +126 -50
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +9 -3
- package/docs/observabilite.md +18 -18
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +115 -78
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +37 -37
- package/package.json +10 -13
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import { isPortListening, resolvePortPolicy } from "nodefony";
|
|
2
|
+
import cluster from "node:cluster";
|
|
2
3
|
//#region nodefony/src/servers/portBinder.ts
|
|
4
|
+
/**
|
|
5
|
+
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
6
|
+
*
|
|
7
|
+
* Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
|
|
8
|
+
* une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
|
|
9
|
+
* en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
|
|
10
|
+
* un CONTRAT en production.
|
|
11
|
+
*
|
|
12
|
+
* ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
|
|
13
|
+
*
|
|
14
|
+
* La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
|
|
15
|
+
* une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
|
|
16
|
+
* prendre le port — et on échoue quand même, après avoir cru le contraire. Le
|
|
17
|
+
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
18
|
+
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
19
|
+
*
|
|
20
|
+
* ## Pourquoi une SONDE malgré cela
|
|
21
|
+
*
|
|
22
|
+
* Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
|
|
23
|
+
* servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
|
|
24
|
+
* processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
|
|
25
|
+
* liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
|
|
26
|
+
* Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
|
|
27
|
+
* l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
|
|
28
|
+
* rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
|
|
29
|
+
*
|
|
30
|
+
* On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
|
|
31
|
+
* liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
|
|
32
|
+
* outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
|
|
33
|
+
* « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
|
|
34
|
+
* atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
|
|
35
|
+
* que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
|
|
36
|
+
* entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
|
|
37
|
+
*
|
|
38
|
+
* ## Pourquoi sauter les ports réservés
|
|
39
|
+
*
|
|
40
|
+
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
41
|
+
* voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
|
|
42
|
+
* AUTRES serveurs convoitent sont donc sautés d'emblée.
|
|
43
|
+
*
|
|
44
|
+
* ## Fail-loud
|
|
45
|
+
*
|
|
46
|
+
* Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
|
|
47
|
+
* là où on l'attend sans le dire est une dégradation silencieuse.
|
|
48
|
+
*/
|
|
3
49
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
4
50
|
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
5
51
|
/** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
|
|
@@ -127,12 +173,21 @@ function isPortUnavailable(code, port) {
|
|
|
127
173
|
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
128
174
|
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
129
175
|
*
|
|
176
|
+
* @param server - serveur à mettre en écoute.
|
|
177
|
+
* @param host - hôte passé à `listen`.
|
|
178
|
+
* @param plan - port désiré et nombre de replis permis.
|
|
179
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
180
|
+
* @param sharedListener - vrai dans un worker `node:cluster` : le port y est
|
|
181
|
+
* PARTAGÉ par le maître, et un voisin qui y répond est un frère, pas un
|
|
182
|
+
* conflit. Sans cette exception, `nodefony cluster -w N` ne gardait qu'un
|
|
183
|
+
* worker en vie — les autres mouraient sur « port déjà servi » et
|
|
184
|
+
* bouclaient en relance.
|
|
130
185
|
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
131
186
|
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
132
187
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
133
188
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
134
189
|
*/
|
|
135
|
-
async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
190
|
+
async function bindWithFallback(server, host, plan, probe = isPortListening, sharedListener = cluster.isWorker) {
|
|
136
191
|
let candidate = plan.desired;
|
|
137
192
|
let used = 0;
|
|
138
193
|
/**
|
|
@@ -154,7 +209,7 @@ async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
|
154
209
|
server.listen(port, host);
|
|
155
210
|
});
|
|
156
211
|
for (;;) {
|
|
157
|
-
const conflict = plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
212
|
+
const conflict = sharedListener || plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
158
213
|
const failure = conflict ? portConflictError(candidate, conflict) : await listenOnce(candidate);
|
|
159
214
|
if (failure === null) return {
|
|
160
215
|
address: server.address(),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { refuseUpgrade } from "../proxy/forward.js";
|
|
2
|
+
import { STATUS_CODES } from "node:http";
|
|
3
|
+
//#region nodefony/src/servers/upgradeDispatch.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
|
|
6
|
+
* puis le serveur WebSocket de Nodefony.
|
|
7
|
+
*
|
|
8
|
+
* Pourquoi un seul écouteur : `ws` attaché par son option `server` prend TOUS
|
|
9
|
+
* les upgrades et répond `400` à ceux qu'il ne reconnaît pas. Un second
|
|
10
|
+
* écouteur, même posé avant, ne l'empêcherait pas de répondre sur un socket
|
|
11
|
+
* déjà relayé. Le serveur `ws` est donc créé en `noServer`, et c'est ici que
|
|
12
|
+
* l'upgrade lui est confié — avec ses propres contrôles intacts (`path`,
|
|
13
|
+
* `verifyClient`, `maxPayload`…), que `handleUpgrade` applique.
|
|
14
|
+
*
|
|
15
|
+
* Entre les deux, `refusal` tranche ce qui doit être refusé AVANT le `101`
|
|
16
|
+
* (RFC 6455 §4.2.2) : un chemin sans route WebSocket reçoit un statut HTTP,
|
|
17
|
+
* jamais une ouverture suivie d'une fermeture.
|
|
18
|
+
*
|
|
19
|
+
* Sans montage, le coût par connexion est une lecture de champ.
|
|
20
|
+
*
|
|
21
|
+
* @param server - serveur HTTP(S) qui reçoit les upgrades
|
|
22
|
+
* @param wss - serveur WebSocket de Nodefony (`noServer: true`)
|
|
23
|
+
* @param relay - proxy inverse, lu à chaque upgrade (il peut naître après)
|
|
24
|
+
* @param refusal - statut HTTP qui refuse l'upgrade, `null` pour l'admettre
|
|
25
|
+
* (cf `HttpKernel.websocketUpgradeRefusal`)
|
|
26
|
+
* @returns la fonction qui retire l'écouteur — à appeler à l'arrêt
|
|
27
|
+
*/
|
|
28
|
+
function attachUpgradeDispatch(server, wss, relay, refusal) {
|
|
29
|
+
const onUpgrade = (req, socket, head) => {
|
|
30
|
+
const proxy = relay();
|
|
31
|
+
if (proxy !== null && proxy.mounts !== null) {
|
|
32
|
+
if (proxy.handleUpgrade(req, socket, head)) return;
|
|
33
|
+
}
|
|
34
|
+
const status = refusal(req);
|
|
35
|
+
if (status !== null) {
|
|
36
|
+
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
40
|
+
wss.emit("connection", ws, req);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
server.on("upgrade", onUpgrade);
|
|
44
|
+
return () => {
|
|
45
|
+
server.removeListener("upgrade", onUpgrade);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { attachUpgradeDispatch };
|
|
@@ -9,6 +9,20 @@ const defaultSessionOptions = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Taille de l'identifiant opaque (octets CSPRNG → base64url, 43 chars). */
|
|
11
11
|
const SESSION_ID_BYTES = 32;
|
|
12
|
+
/** Un sac de session relu du storage : objet, ni `null` ni tableau. */
|
|
13
|
+
function isBag(v) {
|
|
14
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Une date relue du storage : `Date` d'un store mémoire, chaîne ou nombre d'un
|
|
18
|
+
* store sérialisé. Toute autre forme → `undefined` (la valeur courante reste).
|
|
19
|
+
* Convertie UNE fois à la lecture : `created`/`updated` sont alors toujours des
|
|
20
|
+
* `Date`, et les contrôles du chemin chaud lisent `getTime()` sans allouer.
|
|
21
|
+
*/
|
|
22
|
+
function toDate(v) {
|
|
23
|
+
if (v instanceof Date) return v;
|
|
24
|
+
return typeof v === "string" || typeof v === "number" ? new Date(v) : void 0;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Session serveur Nodefony — état persistant lié à une identité, indexé par un
|
|
14
28
|
* identifiant **opaque** (porté par le cookie) dans un {@link ISessionStorage}
|
|
@@ -157,16 +171,19 @@ var Session = class {
|
|
|
157
171
|
* `onSaveSession`.
|
|
158
172
|
*
|
|
159
173
|
* @param user - principal authentifié (string) lié au blob ; défaut courant.
|
|
174
|
+
* Fourni, il DEVIENT le principal de la session : une sauvegarde suivante,
|
|
175
|
+
* dans la même requête, le garde sans qu'on le lui redonne.
|
|
160
176
|
*/
|
|
161
177
|
async save(user) {
|
|
178
|
+
if (user !== void 0) this.user = user;
|
|
162
179
|
if (this.readOnly) {
|
|
163
180
|
if (this.mutated) this.log(`READONLY SESSION mutated — write skipped : ${this.name}`, "WARNING");
|
|
164
181
|
return this;
|
|
165
182
|
}
|
|
166
183
|
if (!this.mutated) return this;
|
|
167
184
|
const stored = await this.storage.write(this.id, this.serialize(user));
|
|
168
|
-
this.created = stored.createdAt ?? this.created;
|
|
169
|
-
this.updated = stored.updatedAt ?? this.updated;
|
|
185
|
+
this.created = toDate(stored.createdAt) ?? this.created;
|
|
186
|
+
this.updated = toDate(stored.updatedAt) ?? this.updated;
|
|
170
187
|
this.mutated = false;
|
|
171
188
|
this.saved = true;
|
|
172
189
|
if (this.context) await this.context.fireAsync("onSaveSession", this);
|
|
@@ -234,15 +251,14 @@ var Session = class {
|
|
|
234
251
|
}
|
|
235
252
|
const now = Date.now();
|
|
236
253
|
if (this.options.absoluteTimeoutS && this.created) {
|
|
237
|
-
if (now -
|
|
254
|
+
if (!(now - this.created.getTime() <= this.options.absoluteTimeoutS * 1e3)) {
|
|
238
255
|
this.log(`SESSION EXPIRED (absolute) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
239
256
|
return false;
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
259
|
const idleS = this.options.idleTimeoutS;
|
|
243
260
|
if (idleS && idleS > 0 && this.updated) {
|
|
244
|
-
|
|
245
|
-
if (lastUsed && lastUsed + idleS * 1e3 < now) {
|
|
261
|
+
if (!(this.updated.getTime() + idleS * 1e3 >= now)) {
|
|
246
262
|
this.log(`SESSION EXPIRED (idle) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
247
263
|
return false;
|
|
248
264
|
}
|
|
@@ -270,7 +286,7 @@ var Session = class {
|
|
|
270
286
|
if (typeof storage.touch !== "function") return;
|
|
271
287
|
if (!this.updated) return;
|
|
272
288
|
const now = Date.now();
|
|
273
|
-
const lastUsed =
|
|
289
|
+
const lastUsed = this.updated.getTime();
|
|
274
290
|
if (lastUsed && now - lastUsed < idleS * 1e3 / 2) return;
|
|
275
291
|
await storage.touch(this.id, idleS);
|
|
276
292
|
this.updated = new Date(now);
|
|
@@ -356,12 +372,12 @@ var Session = class {
|
|
|
356
372
|
}
|
|
357
373
|
deSerialize(serialized) {
|
|
358
374
|
const data = serialized;
|
|
359
|
-
if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
360
|
-
if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
361
|
-
if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
362
|
-
this.created = data.createdAt ?? this.created;
|
|
363
|
-
this.updated = data.updatedAt ?? this.updated;
|
|
364
|
-
if (data.user) this.user = data.user;
|
|
375
|
+
if (isBag(data.Attributes)) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
376
|
+
if (isBag(data.metaBag)) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
377
|
+
if (isBag(data.flashBag)) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
378
|
+
this.created = toDate(data.createdAt) ?? this.created;
|
|
379
|
+
this.updated = toDate(data.updatedAt) ?? this.updated;
|
|
380
|
+
if (typeof data.user === "string" && data.user) this.user = data.user;
|
|
365
381
|
}
|
|
366
382
|
/** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
|
|
367
383
|
clear() {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -76,10 +76,15 @@ export type { DomainPattern, ITrustedHostsConfig, } from "./nodefony/src/context
|
|
|
76
76
|
export { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
77
77
|
export type { IRateLimitStore, IRateLimitOptions, RateLimitVerdict, } from "./nodefony/src/rateLimit/IRateLimitStore.js";
|
|
78
78
|
export { Context, Session, SessionsService, HttpError, HttpKernel, Response, Http2Response, Response as HttpResponse, wsResponse, wsResponse as WebsocketResponse, Cookie, HttpContext, WebsocketContext, HttpRequest, Http2Request, DefaultErrorRenderer, DefaultRequestLogger, JsonAuditLogger, PrettyRequestLogger, Profiler, };
|
|
79
|
+
export { SseStream, openSseStream, acceptsEventStream, SSE_HEARTBEAT_MS, } from "./nodefony/src/context/http/SseStream.js";
|
|
80
|
+
export type { ISseStreamOptions, ISseSendOptions, } from "./nodefony/src/context/http/SseStream.js";
|
|
79
81
|
export { schemaMismatchOf, type SchemaMismatchConfidence, } from "./nodefony/service/error-renderer.js";
|
|
80
82
|
export type { ProfileEntry, ProfileSummary, ProfilePhase, ProfileQuery, ProfileSecurity, } from "./nodefony/src/profiler/Profiler.js";
|
|
81
83
|
export { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
|
|
82
84
|
export type { FrameProfileInit, ProfiledArea, ProfiledResolver, } from "./nodefony/src/profiler/FrameProfile.js";
|
|
85
|
+
export { default as ReverseProxy } from "./nodefony/service/reverse-proxy.js";
|
|
86
|
+
export { parseProxyOrigin, normalizePrefix, proxyMountProblems, } from "./nodefony/src/proxy/rules.js";
|
|
87
|
+
export type { IProxyMount, IProxyMountOptions, ProxyTarget, } from "./nodefony/interfaces/IReverseProxy.js";
|
|
83
88
|
export { resolveUiDelivery, PrebuiltUi, } from "./nodefony/src/assets/prebuiltUi.js";
|
|
84
89
|
export type { UiDeliveryMode, UiDeliveryResolved, IUiDeliveryResolution, IUiDeliveryOptions, IPrebuiltUiOptions, } from "./nodefony/src/assets/prebuiltUi.js";
|
|
85
90
|
export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType, WebSocketStateType, CookiesMap, ISecurityTrace, SecurityOutcome, } from "./nodefony/interfaces/IContext.js";
|
|
@@ -67,6 +67,7 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
67
67
|
key: z.ZodDefault<z.ZodString>;
|
|
68
68
|
cert: z.ZodDefault<z.ZodString>;
|
|
69
69
|
privateKeyMode: z.ZodDefault<z.ZodNumber>;
|
|
70
|
+
extraCaFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
70
71
|
san: z.ZodDefault<z.ZodObject<{
|
|
71
72
|
dns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
72
73
|
ip: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -87,6 +88,7 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
87
88
|
name: z.ZodOptional<z.ZodString>;
|
|
88
89
|
value: z.ZodOptional<z.ZodString>;
|
|
89
90
|
shortName: z.ZodOptional<z.ZodString>;
|
|
91
|
+
type: z.ZodOptional<z.ZodString>;
|
|
90
92
|
}, z.core.$loose>>>;
|
|
91
93
|
}, z.core.$strict>>;
|
|
92
94
|
}, z.core.$strict>>;
|
|
@@ -183,6 +185,21 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
183
185
|
readinessPath: z.ZodDefault<z.ZodString>;
|
|
184
186
|
shutdownDelay: z.ZodDefault<z.ZodNumber>;
|
|
185
187
|
}, z.core.$loose>>;
|
|
188
|
+
proxy: z.ZodDefault<z.ZodObject<{
|
|
189
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
190
|
+
connectTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
191
|
+
maxSockets: z.ZodDefault<z.ZodNumber>;
|
|
192
|
+
mounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
193
|
+
target: z.ZodString;
|
|
194
|
+
methods: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
195
|
+
websocket: z.ZodDefault<z.ZodBoolean>;
|
|
196
|
+
stripPrefix: z.ZodDefault<z.ZodBoolean>;
|
|
197
|
+
preserveHost: z.ZodDefault<z.ZodBoolean>;
|
|
198
|
+
stripHeaders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
199
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
200
|
+
secure: z.ZodDefault<z.ZodBoolean>;
|
|
201
|
+
}, z.core.$strict>>>;
|
|
202
|
+
}, z.core.$strict>>;
|
|
186
203
|
wsMaxConnectionsPerIp: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
187
204
|
}, z.core.$strict>;
|
|
188
205
|
/** Type de sortie (config normalisée + défauts appliqués). */
|
|
@@ -86,6 +86,8 @@ export interface IContext {
|
|
|
86
86
|
cookieSession: ICookie | null | undefined;
|
|
87
87
|
cookies: CookiesMap;
|
|
88
88
|
user: unknown;
|
|
89
|
+
/** Identifiant de l'appelant authentifié (pare-feu), lisible au démontage. */
|
|
90
|
+
userIdentifier: string | null;
|
|
89
91
|
error: Error | null | undefined;
|
|
90
92
|
metaData: object;
|
|
91
93
|
readonly phases: PhaseTiming[];
|
|
@@ -46,7 +46,7 @@ export interface IErrorWebsocketResult {
|
|
|
46
46
|
*
|
|
47
47
|
* Override via `httpKernel.setErrorRenderer(custom)` to customise:
|
|
48
48
|
* - hide stack traces in prod
|
|
49
|
-
* - emit RFC
|
|
49
|
+
* - emit RFC 9457 problem+json
|
|
50
50
|
* - inject auth challenge headers
|
|
51
51
|
*/
|
|
52
52
|
export interface IErrorRenderer {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origine d'un amont relayé : `scheme://hôte[:port]`, sans chemin.
|
|
3
|
+
*
|
|
4
|
+
* Une fonction quand l'origine n'est connue qu'à l'exécution (un serveur
|
|
5
|
+
* enfant qui choisit son port au démarrage) : elle est rappelée à chaque
|
|
6
|
+
* requête, et `undefined` signifie « l'amont n'est pas prêt » — la requête
|
|
7
|
+
* reçoit `503` (`Retry-After: 1`), l'upgrade WebSocket `503` avant tout `101` :
|
|
8
|
+
* le préfixe appartient à l'amont, le routage n'a rien à y répondre.
|
|
9
|
+
*/
|
|
10
|
+
export type ProxyTarget = string | (() => string | undefined);
|
|
11
|
+
/**
|
|
12
|
+
* Réglages d'un montage du proxy inverse (`ReverseProxy.mount`).
|
|
13
|
+
*
|
|
14
|
+
* Les défauts sont ceux d'un relais sans surprise : toutes les méthodes, le
|
|
15
|
+
* chemin transmis tel quel, l'en-tête `Host` réécrit sur la cible, aucun
|
|
16
|
+
* en-tête retiré au-delà des en-têtes de connexion, certificat de la cible
|
|
17
|
+
* vérifié.
|
|
18
|
+
*/
|
|
19
|
+
export interface IProxyMountOptions {
|
|
20
|
+
/** Origine de l'amont (cf {@link ProxyTarget}). */
|
|
21
|
+
target: ProxyTarget;
|
|
22
|
+
/**
|
|
23
|
+
* Méthodes relayées (majuscules). Une autre méthode n'est PAS relayée : la
|
|
24
|
+
* requête poursuit jusqu'au routage. Absent = toutes.
|
|
25
|
+
*/
|
|
26
|
+
methods?: readonly string[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Relaie aussi l'upgrade WebSocket du préfixe. Sans ce drapeau, un upgrade
|
|
29
|
+
* sur le préfixe reste au serveur WebSocket de Nodefony.
|
|
30
|
+
*/
|
|
31
|
+
websocket?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Retire le préfixe avant de transmettre le chemin (`/svc/a` → `/a`).
|
|
34
|
+
* Défaut `false` : l'amont reçoit le chemin complet — c'est le cas d'un
|
|
35
|
+
* serveur qui connaît déjà son préfixe (Vite et son `base`).
|
|
36
|
+
*/
|
|
37
|
+
stripPrefix?: boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Garde l'en-tête `Host` du client au lieu de le réécrire sur la cible.
|
|
40
|
+
* Défaut `false` : un amont qui filtre les noms d'hôte (Vite et son
|
|
41
|
+
* `allowedHosts`) reçoit le sien, et le nom d'origine part dans
|
|
42
|
+
* `X-Forwarded-Host`.
|
|
43
|
+
*/
|
|
44
|
+
preserveHost?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* En-têtes de requête à ne pas transmettre (insensible à la casse) — ce
|
|
47
|
+
* dont l'amont n'a pas l'usage ne doit pas lui parvenir (`cookie`,
|
|
48
|
+
* `authorization` vers un serveur de sources, par exemple).
|
|
49
|
+
*/
|
|
50
|
+
stripHeaders?: readonly string[] | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Délai d'inactivité de l'échange avec l'amont, en ms. Dépassé avant la
|
|
53
|
+
* réponse : 504. Absent = `proxy.timeoutMs` de la configuration (30 s par
|
|
54
|
+
* défaut). Ne s'applique pas à un WebSocket établi.
|
|
55
|
+
*/
|
|
56
|
+
timeoutMs?: number | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Vérifie le certificat d'une cible `https:`. Défaut `true` — `false` n'est
|
|
59
|
+
* ACCEPTÉ que vers la boucle locale (refus au montage sinon).
|
|
60
|
+
*/
|
|
61
|
+
secure?: boolean | undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Un montage tel que le proxy le garde : préfixe normalisé + réglages. */
|
|
64
|
+
export interface IProxyMount extends IProxyMountOptions {
|
|
65
|
+
/** Préfixe d'URL normalisé (`/` en tête et en fin). */
|
|
66
|
+
readonly prefix: string;
|
|
67
|
+
}
|
|
@@ -75,6 +75,22 @@ export interface IRequestRouter {
|
|
|
75
75
|
* @returns le résolveur (`resolve === true` si une route a matché)
|
|
76
76
|
*/
|
|
77
77
|
resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): IRouteResolver;
|
|
78
|
+
/**
|
|
79
|
+
* Dit si au moins une route déclare le transport `WEBSOCKET` pour ce chemin.
|
|
80
|
+
*
|
|
81
|
+
* Lu AVANT le `101` : une socket vers un chemin sans route se refuse par un
|
|
82
|
+
* statut HTTP (RFC 6455 §4.2.2), jamais par une ouverture suivie d'une
|
|
83
|
+
* fermeture — un client qui sonde par une ouverture conclurait que le
|
|
84
|
+
* serveur l'a acceptée. La réponse est un SUR-ENSEMBLE de ce que
|
|
85
|
+
* {@link IRequestRouter.resolve} acceptera : l'hôte (`@Domain`), le
|
|
86
|
+
* sous-protocole et les exigences de variables ne sont pas jugés ici, ils
|
|
87
|
+
* restent des fermetures après ouverture — refuser à tort coûterait une
|
|
88
|
+
* connexion légitime.
|
|
89
|
+
*
|
|
90
|
+
* @param pathname - chemin de l'URL d'upgrade (WHATWG), barres finales comprises
|
|
91
|
+
* @returns `false` si aucune route WebSocket ne peut répondre à ce chemin
|
|
92
|
+
*/
|
|
93
|
+
servesWebsocket(pathname: string): boolean;
|
|
78
94
|
/**
|
|
79
95
|
* Instance partagée d'un controller `scope: "singleton"`, créée une seule fois.
|
|
80
96
|
*
|
|
@@ -42,4 +42,10 @@ export interface IFirewallGate {
|
|
|
42
42
|
handleCors(context: ContextType): number | undefined;
|
|
43
43
|
/** Pose les en-têtes de sécurité (CSP, HSTS…) de la réponse. */
|
|
44
44
|
applySecurityHeaders(context: ContextType): void;
|
|
45
|
+
/**
|
|
46
|
+
* Le revalidateur de l'identité authentifiée de la requête courante, pour une
|
|
47
|
+
* connexion longue qui la gardera (flux SSE) : la même règle que les sockets
|
|
48
|
+
* WebSocket. `null` pour un anonyme — rien à révoquer.
|
|
49
|
+
*/
|
|
50
|
+
currentRevalidator?(): ((nowMs?: number) => Promise<boolean>) | null;
|
|
45
51
|
}
|
|
@@ -7,3 +7,4 @@ export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType,
|
|
|
7
7
|
export type { IHttpKernel } from "./IHttpKernel.js";
|
|
8
8
|
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./IRouting.js";
|
|
9
9
|
export type { ISecurityZone, IFirewallGate } from "./ISecurity.js";
|
|
10
|
+
export type { IProxyMount, IProxyMountOptions, ProxyTarget, } from "./IReverseProxy.js";
|
|
@@ -17,7 +17,8 @@ import type { IHttpContext, IWebsocketContext, PhaseTiming } from "../interfaces
|
|
|
17
17
|
export interface AuditLogEntry {
|
|
18
18
|
ts: string;
|
|
19
19
|
requestId: string;
|
|
20
|
-
|
|
20
|
+
/** Identifiant de l'appelant authentifié (`context.userIdentifier`), `null` sinon. */
|
|
21
|
+
userIdentifier: string | null;
|
|
21
22
|
type: "http" | "ws";
|
|
22
23
|
scheme: string;
|
|
23
24
|
method: string | null;
|