@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/config/config.js +44 -4
- 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 +140 -15
- 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/upload/upload-service.js +3 -3
- 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 +1 -9
- package/dist/nodefony/src/context/http/Response.js +16 -1
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- 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/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 +16 -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 +96 -5
- 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/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/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 +5 -1
- package/docs/observabilite.md +15 -15
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +94 -60
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +23 -23
- package/package.json +8 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePrefix, parseProxyOrigin, proxyMountProblems } from "./nodefony/src/proxy/rules.js";
|
|
1
2
|
import config, { httpConfigSchema, httpOverlaySchema } from "./nodefony/config/config.js";
|
|
2
3
|
import { defineHttpConfig, httpConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
3
4
|
import { createHttpAdminApi } from "./nodefony/service/HttpAdminApi.js";
|
|
@@ -16,19 +17,21 @@ import HttpRequest from "./nodefony/src/context/http/Request.js";
|
|
|
16
17
|
import HttpResponse from "./nodefony/src/context/http/Response.js";
|
|
17
18
|
import Http2Request from "./nodefony/src/context/http2/Request.js";
|
|
18
19
|
import Http2Response from "./nodefony/src/context/http2/Response.js";
|
|
20
|
+
import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
|
|
19
21
|
import HttpContext from "./nodefony/src/context/http/HttpContext.js";
|
|
20
22
|
import DefaultErrorRenderer, { schemaMismatchOf } from "./nodefony/service/error-renderer.js";
|
|
21
23
|
import DefaultRequestLogger from "./nodefony/service/request-logger.js";
|
|
22
24
|
import JsonAuditLogger from "./nodefony/service/audit-logger.js";
|
|
23
25
|
import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
|
|
24
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
25
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
26
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
27
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
26
28
|
import http_kernel_default from "./nodefony/service/http-kernel.js";
|
|
27
29
|
import server_http_default from "./nodefony/service/servers/server-http.js";
|
|
28
30
|
import server_https_default from "./nodefony/service/servers/server-https.js";
|
|
29
31
|
import server_websocket_default from "./nodefony/service/servers/server-websocket.js";
|
|
30
32
|
import server_websocket_secure_default from "./nodefony/service/servers/server-websocket-secure.js";
|
|
31
33
|
import Statics from "./nodefony/service/servers/server-static.js";
|
|
34
|
+
import ReverseProxy from "./nodefony/service/reverse-proxy.js";
|
|
32
35
|
import Network from "./nodefony/command/networkCommand.js";
|
|
33
36
|
import Certificates from "./nodefony/command/certificatesCommand.js";
|
|
34
37
|
import ProxyGenerate from "./nodefony/command/proxyGenerateCommand.js";
|
|
@@ -36,7 +39,7 @@ import AssetsPublish from "./nodefony/command/assetsPublishCommand.js";
|
|
|
36
39
|
import Session from "./nodefony/src/session/session.js";
|
|
37
40
|
import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS } from "./nodefony/src/session/storage/sessionSort.js";
|
|
38
41
|
import sessions_service_default from "./nodefony/service/sessions/sessions-service.js";
|
|
39
|
-
import Certificate from "./nodefony/service/certificates.js";
|
|
42
|
+
import Certificate, { trustOutboundAuthorities } from "./nodefony/service/certificates.js";
|
|
40
43
|
import upload_service_default from "./nodefony/service/upload/upload-service.js";
|
|
41
44
|
import { PrebuiltUi, resolveUiDelivery } from "./nodefony/src/assets/prebuiltUi.js";
|
|
42
45
|
import { Kernel, Module, services } from "nodefony";
|
|
@@ -70,7 +73,9 @@ let Http = class Http extends Module {
|
|
|
70
73
|
*/
|
|
71
74
|
async onKernelRegister() {
|
|
72
75
|
const input = this.options;
|
|
73
|
-
|
|
76
|
+
const options = defineHttpConfig(input ?? {}, this.kernel);
|
|
77
|
+
this.options = options;
|
|
78
|
+
await trustOutboundAuthorities(options.certificates.extraCaFiles, this.kernel?.path ?? process.cwd(), (message, severity) => this.log(message, severity));
|
|
74
79
|
return this;
|
|
75
80
|
}
|
|
76
81
|
/**
|
|
@@ -99,6 +104,7 @@ Http = __decorate([services([
|
|
|
99
104
|
Certificate,
|
|
100
105
|
sessions_service_default,
|
|
101
106
|
Statics,
|
|
107
|
+
ReverseProxy,
|
|
102
108
|
server_http_default,
|
|
103
109
|
server_https_default,
|
|
104
110
|
server_websocket_default,
|
|
@@ -107,4 +113,4 @@ Http = __decorate([services([
|
|
|
107
113
|
]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Http);
|
|
108
114
|
var http_default = Http;
|
|
109
115
|
//#endregion
|
|
110
|
-
export { Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, PrebuiltUi, PrettyRequestLogger, Profiler, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, Session, sessions_service_default as SessionsService, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, decideSend, http_default as default, defineHttpConfig, httpConfigJsonSchema, httpConfigSchema, isDomainAllowed, readBackpressureOptions, resolveUiDelivery, schemaMismatchOf };
|
|
116
|
+
export { Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, PrebuiltUi, PrettyRequestLogger, Profiler, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, decideSend, http_default as default, defineHttpConfig, httpConfigJsonSchema, httpConfigSchema, isDomainAllowed, normalizePrefix, openSseStream, parseProxyOrigin, proxyMountProblems, readBackpressureOptions, resolveUiDelivery, schemaMismatchOf };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePrefix, proxyMountProblems } from "../src/proxy/rules.js";
|
|
1
2
|
import { overlayField } from "nodefony";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region nodefony/config/config.ts
|
|
@@ -182,7 +183,7 @@ const sanSchema = z.strictObject({
|
|
|
182
183
|
description: "Noms DNS du Subject Alternative Name (RFC 5280 §4.2.1.6). Vide = dérivé du kernel (localhost + domain)."
|
|
183
184
|
}),
|
|
184
185
|
ip: z.array(z.string()).default([]).describe("Adresses IP du SAN. Vide = dérivé (127.0.0.1, ::1).")
|
|
185
|
-
}).describe("Subject Alternative Name explicite. Vide = dérivé (localhost + domaine kernel). Chrome ignore le commonName
|
|
186
|
+
}).describe("Subject Alternative Name explicite. Vide = dérivé (localhost + domaine kernel). Chrome ignore le commonName ; le SAN seul fait foi (RFC 9525).");
|
|
186
187
|
const certDevSchema = z.strictObject({ useMkcert: z.boolean().default(true).describe("Préférer mkcert (CA locale trustée) en dev → HTTPS sans erreur navigateur (HMR/WSS). false = forcer le fallback auto-signé. Ignoré hors dev.") }).describe("Options de génération du certificat en développement.");
|
|
187
188
|
const certificatesSchema = z.strictObject({
|
|
188
189
|
strategy: z.enum([
|
|
@@ -195,6 +196,7 @@ const certificatesSchema = z.strictObject({
|
|
|
195
196
|
key: z.string().default("").describe("Chemin de la clé privée TLS. Vide = générée."),
|
|
196
197
|
cert: z.string().default("").describe("Chemin du certificat TLS. Vide = généré."),
|
|
197
198
|
privateKeyMode: z.number().int().default(384).meta({ description: "Permissions POSIX de la clé privée écrite sur disque (0600 = lecture/écriture owner uniquement). Une clé TLS ne doit JAMAIS être world-readable." }),
|
|
199
|
+
extraCaFiles: z.array(z.string().min(1)).default([]).describe("Autorités de certification supplémentaires pour les requêtes SORTANTES du processus (découverte OpenID, jeton, jeu de clés, API tierces) : fichiers PEM, absolus ou relatifs à la racine de l'application, un fichier pouvant porter une chaîne entière. L'équivalent porté par l'application de NODE_EXTRA_CA_CERTS — sans syntaxe de shell, donc identique sous Windows. Fichier illisible ou sans certificat = refus au démarrage. La CA de développement générée par Nodefony n'a pas besoin d'y figurer."),
|
|
198
200
|
san: sanSchema.default(() => sanSchema.parse({})),
|
|
199
201
|
dev: certDevSchema.default(() => certDevSchema.parse({})),
|
|
200
202
|
selfSigned: selfSignedSchema.default(() => selfSignedSchema.parse({}))
|
|
@@ -221,9 +223,9 @@ const websocketSchema = z.looseObject({
|
|
|
221
223
|
autoPong: z.boolean().default(true).describe("Répond automatiquement par un pong à chaque ping entrant (RFC 6455 §5.5.2-5.5.3). Laisser true — requis par le protocole et par le heartbeat des pairs."),
|
|
222
224
|
allowSynchronousEvents: z.boolean().default(true).describe("Émet les events ('message', 'ping'…) de plusieurs frames lues dans un même chunk réseau de façon synchrone (ws@8.18+). true = meilleur débit ; false = un event par tick d'event loop (latence plus régulière, plus équitable entre connexions)."),
|
|
223
225
|
maxBackpressure: z.number().int().nonnegative().default(4194304).describe("Seuil (octets) du buffer d'envoi par connexion (`ws.bufferedAmount`) au-delà duquel `backpressurePolicy` s'applique. Borne la RAM d'envoi face à un client LENT À RECEVOIR (anti-OOM) ; broadcast() amplifie (un seul lent peut plomber la diffusion). 0 = désactivé. Défaut 4 MiB."),
|
|
224
|
-
backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("
|
|
226
|
+
backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("SOLDE de frames refusées au-delà duquel la connexion est FERMÉE (1013), en politique 'drop' : +1 par frame refusée, −1 par frame envoyée. POURQUOI un solde et pas un second seuil d'octets : une fois qu'on jette, plus rien n'alimente la file — elle plafonne au seuil de drop et n'atteint jamais un seuil supérieur, qui serait donc inatteignable (mesuré). Et pas une série remise à zéro au premier envoi : la file d'un client bloqué oscille autour du seuil, chaque envoi effacerait la série et il ne serait jamais fermé. Le solde, lui, distingue le pic passager du client mort. 0 = ne jamais fermer. Défaut 1000."),
|
|
225
227
|
backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport.")
|
|
226
|
-
}).describe("Serveur WebSocket (`ws@8`). Les options ci-dessus (+ loose : `verifyClient`, `handleProtocols`, `path`, `WebSocket`…) sont transmises à `ws`. GÉRÉ par Nodefony et donc NON exposé : `server`/`host`/`port` (
|
|
228
|
+
}).describe("Serveur WebSocket (`ws@8`). Les options ci-dessus (+ loose : `verifyClient`, `handleProtocols`, `path`, `WebSocket`…) sont transmises à `ws`. GÉRÉ par Nodefony et donc NON exposé : `server`/`host`/`port`/`noServer` (forcé à `noServer` : l'upgrade du serveur HTTP passe d'abord par le proxy inverse, puis par `ws`), `clientTracking` (forcé true — requis par broadcast() + heartbeat), `backlog` (celui du serveur HTTP). `keepalive*`/`closeTimeout` sont des knobs Nodefony, pas des options `ws`.");
|
|
227
229
|
const staticOptionsSchema = z.looseObject({
|
|
228
230
|
cacheControl: z.boolean().optional().describe("Active le header Cache-Control."),
|
|
229
231
|
maxAge: z.number().int().nonnegative().optional().describe("Durée de cache (selon l'entrée : secondes pour defaultOptions, ms côté serve-static).")
|
|
@@ -287,6 +289,43 @@ const healthSchema = z.looseObject({
|
|
|
287
289
|
readinessPath: z.string().default("/readyz").describe("Chemin de la probe readiness (k8s `readinessProbe.httpGet.path`). 200 quand le boot est complet (`onPostReady`) ; 503 avant, et dès le début du shutdown (le LB retire le pod AVANT le drain)."),
|
|
288
290
|
shutdownDelay: z.number().int().nonnegative().default(0).describe("Délai (ms) entre la bascule readiness→503 et le début du drain — laisse aux endpoints k8s/LB le temps de retirer le pod (fenêtre réelle de propagation ~1-2 s). 0 (défaut) = drain immédiat ; alternative opérationnelle : `lifecycle.preStop: sleep N` dans le pod spec. À additionner au budget < grace period (30 s k8s).")
|
|
289
291
|
}).describe("Probes de santé cloud-native `/livez` + `/readyz` (liveness/readiness k8s, HEALTHCHECK Docker) — servies par les serveurs HTTP et HTTPS.");
|
|
292
|
+
const proxyMountSchema = z.strictObject({
|
|
293
|
+
target: z.string().describe("Origine de l'amont : `scheme://hôte[:port]`, http ou https, SANS chemin (ex. `http://127.0.0.1:8080`). Le chemin relayé est celui de la requête (cf `stripPrefix`)."),
|
|
294
|
+
methods: z.array(z.string()).optional().describe("Méthodes relayées (casse indifférente). Une autre méthode n'est PAS relayée : la requête suit le routage de l'application (404 si rien ne la prend). Absent = toutes."),
|
|
295
|
+
websocket: z.boolean().default(false).describe("Relaie aussi l'upgrade WebSocket du préfixe (même origine, même certificat que la page). Défaut false : un upgrade sur le préfixe reste au serveur WebSocket de Nodefony."),
|
|
296
|
+
stripPrefix: z.boolean().default(false).describe("Retire le préfixe avant de transmettre (`/billing/factures` → `/factures`). Défaut false : l'amont reçoit le chemin complet. ⚠️ Un amont qui ignore son préfixe rendra des liens absolus (`/x`) et des redirections qui sortent du montage."),
|
|
297
|
+
preserveHost: z.boolean().default(false).describe("Transmet le `Host` du client au lieu de celui de la cible. Défaut false : l'amont reçoit son propre nom (ce qu'attend un serveur qui filtre les noms d'hôte), le nom d'origine part en `X-Forwarded-Host`."),
|
|
298
|
+
stripHeaders: z.array(z.string()).default([]).describe("En-têtes de requête à ne PAS transmettre (ex. `[\"cookie\", \"authorization\"]` vers un amont qui n'a pas à voir la session). Les en-têtes de connexion (RFC 9110 §7.6.1) ne le sont jamais."),
|
|
299
|
+
timeoutMs: z.number().optional().describe("Délai d'inactivité avec l'amont (ms) — dépassé avant la réponse (ou avant le `101` d'un upgrade WebSocket) : 504. Absent = `proxy.timeoutMs`. Ne s'applique pas à un WebSocket établi."),
|
|
300
|
+
secure: z.boolean().default(true).describe("Vérifie le certificat d'une cible https. `false` n'est accepté que vers la boucle locale (`127.0.0.0/8`, `localhost`, `::1`) ; ailleurs, fournir l'autorité de l'amont (`certificates.extraCaFiles`, ou `NODE_EXTRA_CA_CERTS`).")
|
|
301
|
+
}).describe("Un montage du proxy inverse : un préfixe d'URL relayé vers un amont.");
|
|
302
|
+
const proxySchema = z.strictObject({
|
|
303
|
+
timeoutMs: z.number().int().positive().default(3e4).describe("Délai d'inactivité par défaut de tous les montages (ms) : un amont muet au-delà rend 504 Gateway Timeout. Défaut 30 s."),
|
|
304
|
+
connectTimeoutMs: z.number().int().positive().default(5e3).describe("Délai d'ÉTABLISSEMENT de la connexion vers un amont (ms), distinct du délai d'inactivité : un amont qui n'accepte pas la connexion rend 504 sans tenir le client pendant `timeoutMs`. Défaut 5 s."),
|
|
305
|
+
maxSockets: z.number().int().positive().default(256).describe("Connexions simultanées maximales vers UN amont (pool keep-alive dédié par montage) ; au-delà, les requêtes attendent un socket libre. Borne aussi les tunnels WebSocket relayés, hors pool : au-delà, l'upgrade rend 503. Défaut 256. Le pool ignore `HTTP_PROXY`/`NODE_USE_ENV_PROXY` : un amont se joint en direct."),
|
|
306
|
+
mounts: z.record(z.string(), proxyMountSchema).default({}).describe("Préfixe d'URL → amont. Consulté AVANT le routage : une route de l'application sous ce préfixe ne sera jamais atteinte. ⚠️ Servi AVANT le pare-feu, la CSRF et les en-têtes applicatifs (CSP, Referrer-Policy, COOP) — l'amont porte sa propre politique d'accès ; les en-têtes de transport (nosniff, X-Frame-Options, HSTS) sont posés si l'amont ne les fixe pas, et les quotas (rate-limit, connexions WebSocket par IP) s'appliquent. Correspondance au préfixe le PLUS LONG. Refusés au démarrage : `/`, ce qui est sous `/nodefony/` (administration) ou `/_vite/` (monté par @nodefony/frontend), deux clés qui désignent le même préfixe.")
|
|
307
|
+
}).superRefine((proxy, ctx) => {
|
|
308
|
+
const seen = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const [prefix, mount] of Object.entries(proxy.mounts)) {
|
|
310
|
+
const normalized = normalizePrefix(prefix);
|
|
311
|
+
const twin = seen.get(normalized);
|
|
312
|
+
if (twin !== void 0) ctx.addIssue({
|
|
313
|
+
code: "custom",
|
|
314
|
+
path: ["mounts", prefix],
|
|
315
|
+
message: `« ${prefix} » et « ${twin} » désignent le même préfixe (${normalized}) — n'en garder qu'un`
|
|
316
|
+
});
|
|
317
|
+
seen.set(normalized, prefix);
|
|
318
|
+
for (const problem of proxyMountProblems(prefix, mount, "config")) ctx.addIssue({
|
|
319
|
+
code: "custom",
|
|
320
|
+
path: problem.field === "prefix" ? ["mounts", prefix] : [
|
|
321
|
+
"mounts",
|
|
322
|
+
prefix,
|
|
323
|
+
problem.field
|
|
324
|
+
],
|
|
325
|
+
message: problem.message
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}).describe("Proxy inverse : relaie un préfixe d'URL (requêtes et, sur demande, WebSocket) vers un autre serveur, sur la MÊME origine que la page — un seul certificat, l'amont peut rester sur la boucle locale. En-têtes de connexion jamais transmis, `X-Forwarded-*` et `Via` posés, `trustedHosts` appliqué, 502/504 sur amont injoignable/muet, boucle détectée (508).");
|
|
290
329
|
const httpConfigSchema = z.strictObject({
|
|
291
330
|
headerServer: z.string().nullable().default("nodefony").meta({
|
|
292
331
|
runtimeMutable: true,
|
|
@@ -323,9 +362,10 @@ const httpConfigSchema = z.strictObject({
|
|
|
323
362
|
session: sessionSchema.default(() => sessionSchema.parse({})),
|
|
324
363
|
rateLimit: rateLimitSchema.default(() => rateLimitSchema.parse({})),
|
|
325
364
|
health: healthSchema.default(() => healthSchema.parse({})),
|
|
365
|
+
proxy: proxySchema.default(() => proxySchema.parse({})),
|
|
326
366
|
wsMaxConnectionsPerIp: z.number().int().positive().nullable().default(null).meta({
|
|
327
367
|
runtimeMutable: true,
|
|
328
|
-
description: "Backstop OPT-IN : plafond de connexions
|
|
368
|
+
description: "Backstop OPT-IN : plafond de connexions LONGUES CONCURRENTES par IP cliente — sockets WebSocket ET flux SSE, un seul budget pour les deux. `null` (DÉFAUT) = désactivé. Distinct de `rateLimit` (qui borne le DÉBIT d'ouverture par fenêtre) : ici on borne le NOMBRE de connexions simultanément ouvertes par IP. Au-delà, l'upgrade WebSocket est fermé (RFC 6455 close 1013) et un flux SSE répond 429. ⚠️ PORTÉE PAR PROCESS (1 pod) : un vrai plafond GLOBAL/IP se fait à l'INGRESS/LB — nginx `limit_conn`, HAProxy `sc_conn_cur`, annotation k8s `nginx.ingress.kubernetes.io/limit-connections` — qui voit TOUT le trafic, rejette AVANT que l'app paie le fd + le handshake TLS, et couvre tous les pods. En cloud-native, laisser `null` et déléguer à l'edge. N'activer (ex. 20) que sur bare-metal/VPS SANS ingress, comme défense en profondeur. IP résolue forwarded-aware (RFC 7239 + trustProxy)."
|
|
329
369
|
})
|
|
330
370
|
}).describe("Configuration de @nodefony/http.");
|
|
331
371
|
/**
|
|
@@ -11,7 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
* Sans ce remplissage, le certificat auto-signé n'aurait pas de commonName.
|
|
12
12
|
* - `certificates.san` vide → Subject Alternative Name dérivé du kernel
|
|
13
13
|
* (`localhost` + `kernel.domain` en DNS ; `127.0.0.1`/`::1` en IP). Le SAN fait
|
|
14
|
-
* foi pour la vérification d'hôte (RFC
|
|
14
|
+
* foi pour la vérification d'hôte (RFC 9525) — Chrome ignore le commonName.
|
|
15
15
|
* `0.0.0.0` (bind toutes interfaces) n'est PAS un nom DNS valide → exclu.
|
|
16
16
|
*
|
|
17
17
|
* Muté EN PLACE (pas de copie) : la config est ré-assignée à `module.options`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -25,7 +25,6 @@ function pageParams(query) {
|
|
|
25
25
|
function adminActor(user) {
|
|
26
26
|
if (user && typeof user === "object") {
|
|
27
27
|
const u = user;
|
|
28
|
-
if (typeof u.username === "string" && u.username) return u.username;
|
|
29
28
|
if (typeof u.identifier === "string" && u.identifier) return u.identifier;
|
|
30
29
|
}
|
|
31
30
|
return "admin";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { RequestContext } from "nodefony";
|
|
2
1
|
import { performance } from "node:perf_hooks";
|
|
3
2
|
//#region nodefony/service/audit-logger.ts
|
|
4
3
|
/**
|
|
@@ -84,7 +83,7 @@ var JsonAuditLogger = class {
|
|
|
84
83
|
const entry = {
|
|
85
84
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
86
85
|
requestId: ctx.requestId,
|
|
87
|
-
|
|
86
|
+
userIdentifier: ctx.userIdentifier ?? null,
|
|
88
87
|
type: "http",
|
|
89
88
|
scheme: ctx.scheme,
|
|
90
89
|
method: ctx.method,
|
|
@@ -115,7 +114,7 @@ var JsonAuditLogger = class {
|
|
|
115
114
|
const entry = {
|
|
116
115
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
117
116
|
requestId: ctx.requestId,
|
|
118
|
-
|
|
117
|
+
userIdentifier: ctx.userIdentifier ?? null,
|
|
119
118
|
type: "ws",
|
|
120
119
|
scheme: ctx.scheme,
|
|
121
120
|
method: ctx.method,
|
|
@@ -1,17 +1,125 @@
|
|
|
1
1
|
import { asServersConfig } from "../src/servers/kernelServers.js";
|
|
2
2
|
import { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid } from "./x509.js";
|
|
3
|
-
import { Service, extend } from "nodefony";
|
|
3
|
+
import { BootConfigurationError, Service, extend } from "nodefony";
|
|
4
4
|
import { X509Certificate, generateKeyPair, randomBytes } from "node:crypto";
|
|
5
5
|
import fsp from "node:fs/promises";
|
|
6
6
|
import path, { resolve } from "node:path";
|
|
7
|
+
import tls from "node:tls";
|
|
7
8
|
import { promisify } from "node:util";
|
|
8
9
|
import { execFile } from "node:child_process";
|
|
9
10
|
//#region nodefony/service/certificates.ts
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
const generateKeyPairAsync = promisify(generateKeyPair);
|
|
13
|
+
/** Compare deux PEM sans dépendre des fins de ligne ni des espaces. */
|
|
14
|
+
function normalizePem(pem) {
|
|
15
|
+
return pem.replace(/\s+/g, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ajoute une autorité aux ancres TLS par défaut du processus (requêtes sortantes
|
|
19
|
+
* `fetch`, `https`, `tls.connect`), sans en retirer aucune.
|
|
20
|
+
*
|
|
21
|
+
* C'est l'équivalent en cours d'exécution de `NODE_EXTRA_CA_CERTS`, qui n'est lu
|
|
22
|
+
* qu'au lancement du processus — trop tôt pour une CA que le boot vient de
|
|
23
|
+
* générer, et que seul un script de banc posait. La capacité se CONSTATE
|
|
24
|
+
* (`tls.setDefaultCACertificates` n'existe qu'à partir de Node 24.5), elle ne se
|
|
25
|
+
* déduit pas d'un numéro de version. Idempotent : une autorité déjà présente
|
|
26
|
+
* n'est pas ajoutée deux fois.
|
|
27
|
+
*
|
|
28
|
+
* @param pem - autorité au format PEM.
|
|
29
|
+
* @param store - accès aux ancres ; `node:tls` par défaut.
|
|
30
|
+
* @returns `added`, `present` (déjà de confiance), ou `unsupported`.
|
|
31
|
+
*/
|
|
32
|
+
function addDefaultCaCertificate(pem, store = tls) {
|
|
33
|
+
const { getCACertificates, setDefaultCACertificates } = store;
|
|
34
|
+
if (typeof getCACertificates !== "function" || typeof setDefaultCACertificates !== "function") return "unsupported";
|
|
35
|
+
const current = getCACertificates("default");
|
|
36
|
+
const wanted = normalizePem(pem);
|
|
37
|
+
if (current.some((c) => normalizePem(c) === wanted)) return "present";
|
|
38
|
+
setDefaultCACertificates([...current, pem]);
|
|
39
|
+
return "added";
|
|
40
|
+
}
|
|
41
|
+
const PEM_CERTIFICATE = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;
|
|
42
|
+
/**
|
|
43
|
+
* Découpe un fichier PEM en ses certificats — une chaîne d'entreprise est
|
|
44
|
+
* souvent livrée en un seul fichier (racine + intermédiaires).
|
|
45
|
+
*
|
|
46
|
+
* Chaque certificat est ajouté SÉPARÉMENT : c'est ce qui rend l'ajout
|
|
47
|
+
* idempotent certificat par certificat, la liste par défaut du processus
|
|
48
|
+
* n'étant faite que de certificats isolés.
|
|
49
|
+
*
|
|
50
|
+
* @param pem - contenu d'un fichier PEM (un ou plusieurs certificats)
|
|
51
|
+
* @returns les certificats, dans l'ordre du fichier ; vide s'il n'y en a aucun
|
|
52
|
+
*/
|
|
53
|
+
function splitPemCertificates(pem) {
|
|
54
|
+
return pem.match(PEM_CERTIFICATE) ?? [];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ajoute les autorités d'un fichier PEM aux ancres TLS du processus
|
|
58
|
+
* (`certificates.extraCaFiles`) — l'équivalent, porté par l'APPLICATION, de
|
|
59
|
+
* `NODE_EXTRA_CA_CERTS`.
|
|
60
|
+
*
|
|
61
|
+
* `NODE_EXTRA_CA_CERTS` n'est lu qu'au lancement de Node, et se pose dans la
|
|
62
|
+
* syntaxe du shell — `VAR=… npm run dev` n'existe pas sous Windows. Une autorité
|
|
63
|
+
* d'entreprise (Keycloak interne, proxy sortant qui ré-signe) se déclare donc
|
|
64
|
+
* dans la configuration, et sert à TOUTES les requêtes sortantes du processus :
|
|
65
|
+
* découverte OpenID, point de jeton, jeu de clés, appels d'administration.
|
|
66
|
+
*
|
|
67
|
+
* @param file - chemin du fichier, absolu ou relatif à `root`
|
|
68
|
+
* @param root - racine de l'application
|
|
69
|
+
* @param store - accès aux ancres ; `node:tls` par défaut
|
|
70
|
+
* @returns le bilan de l'ajout
|
|
71
|
+
* @throws BootConfigurationError - fichier illisible, sans certificat, ou Node
|
|
72
|
+
* incapable d'élargir ses ancres en cours d'exécution
|
|
73
|
+
*/
|
|
74
|
+
async function trustCaFile(file, root, store = tls) {
|
|
75
|
+
const absolute = path.resolve(root, file);
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
content = await fsp.readFile(absolute, "utf8");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
81
|
+
const reason = typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
82
|
+
throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » est illisible (${reason}).`);
|
|
83
|
+
}
|
|
84
|
+
const certificates = splitPemCertificates(content);
|
|
85
|
+
if (certificates.length === 0) throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » ne contient aucun certificat PEM (-----BEGIN CERTIFICATE-----).`);
|
|
86
|
+
let added = 0;
|
|
87
|
+
let present = 0;
|
|
88
|
+
for (const pem of certificates) {
|
|
89
|
+
const verdict = addDefaultCaCertificate(pem, store);
|
|
90
|
+
if (verdict === "unsupported") throw new BootConfigurationError(`http.certificates.extraCaFiles : ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5). Mettre Node à jour, ou lancer avec NODE_EXTRA_CA_CERTS=${absolute}.`);
|
|
91
|
+
if (verdict === "added") added += 1;
|
|
92
|
+
else present += 1;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
file: absolute,
|
|
96
|
+
added,
|
|
97
|
+
present
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ajoute aux ancres TLS du processus les autorités déclarées
|
|
102
|
+
* (`certificates.extraCaFiles`), au plus tôt : appelé à l'enregistrement du
|
|
103
|
+
* module, avant tout service — toute requête sortante, commande console
|
|
104
|
+
* comprise, les trouve en place.
|
|
105
|
+
*
|
|
106
|
+
* La CA de DÉVELOPPEMENT n'y figure pas : le service de certificats la pose
|
|
107
|
+
* lui-même ({@link Certificate.generateServerCertificates}).
|
|
108
|
+
*
|
|
109
|
+
* @param files - `certificates.extraCaFiles`
|
|
110
|
+
* @param root - racine de l'application
|
|
111
|
+
* @param log - journal du module
|
|
112
|
+
* @throws BootConfigurationError - cf {@link trustCaFile}
|
|
113
|
+
*/
|
|
114
|
+
async function trustOutboundAuthorities(files, root, log) {
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const trust = await trustCaFile(file, root);
|
|
117
|
+
log(`autorité sortante ${trust.file} : ${trust.added} ajoutée(s), ${trust.present} déjà présente(s)`, "INFO");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
12
120
|
/**
|
|
13
121
|
* Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
|
|
14
|
-
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC
|
|
122
|
+
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 9525).
|
|
15
123
|
*/
|
|
16
124
|
function isIpLiteral(host) {
|
|
17
125
|
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
|
@@ -44,7 +152,7 @@ const defaultOptions = {
|
|
|
44
152
|
*
|
|
45
153
|
* Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
|
|
46
154
|
* RFC 5280 / CA-B Forum), numéro de série aléatoire 128 bits unique
|
|
47
|
-
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC
|
|
155
|
+
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 9525), `notBefore` reculé,
|
|
48
156
|
* clé privée écrite en `0600`.
|
|
49
157
|
*/
|
|
50
158
|
var Certificate = class Certificate extends Service {
|
|
@@ -56,8 +164,6 @@ var Certificate = class Certificate extends Service {
|
|
|
56
164
|
cert = "";
|
|
57
165
|
fullchainPem = "";
|
|
58
166
|
publicKeyPem = "";
|
|
59
|
-
rootCertPem = "";
|
|
60
|
-
intermediateCertPem = "";
|
|
61
167
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
62
168
|
mkcertCaRoot = null;
|
|
63
169
|
path = resolve(".", "nodefony", "config", "certificates");
|
|
@@ -181,7 +287,11 @@ var Certificate = class Certificate extends Service {
|
|
|
181
287
|
await this.ensureDirectoriesExist();
|
|
182
288
|
const strategy = await this.resolveStrategy();
|
|
183
289
|
if (strategy === "explicit") return this.loadExplicitCert();
|
|
184
|
-
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy))
|
|
290
|
+
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) {
|
|
291
|
+
await this.readCerticates();
|
|
292
|
+
this.trustDevelopmentAuthority();
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
185
295
|
if (strategy === "mkcert") await this.generateWithMkcert();
|
|
186
296
|
else {
|
|
187
297
|
this.keysPair = await this.generateKeys();
|
|
@@ -195,7 +305,26 @@ var Certificate = class Certificate extends Service {
|
|
|
195
305
|
await this.writeCertificates(true);
|
|
196
306
|
await fsp.writeFile(this.caPath, certPem.toString(), "utf8");
|
|
197
307
|
}
|
|
198
|
-
|
|
308
|
+
await this.readCerticates();
|
|
309
|
+
this.trustDevelopmentAuthority();
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* En développement, fait confiance à la CA que Nodefony vient de générer ou de
|
|
314
|
+
* relire, pour les requêtes SORTANTES du processus.
|
|
315
|
+
*
|
|
316
|
+
* Sans elle, un service du décor qui sert le certificat de l'application
|
|
317
|
+
* (Keycloak, l'auto-vérification d'un jeton MCP) est injoignable depuis
|
|
318
|
+
* `nodefony development` — `fetch failed` / `SELF_SIGNED_CERT_IN_CHAIN` — alors
|
|
319
|
+
* que `curl -k` répond : la confiance n'existait que sous le script de banc.
|
|
320
|
+
* Réservé au DÉVELOPPEMENT et aux certificats que Nodefony fabrique : un
|
|
321
|
+
* certificat `explicit` (production) n'élargit jamais la confiance du processus.
|
|
322
|
+
*/
|
|
323
|
+
trustDevelopmentAuthority() {
|
|
324
|
+
if (!this.isDev() || !this.ca || this.ca.length === 0) return;
|
|
325
|
+
const verdict = addDefaultCaCertificate(this.ca.toString());
|
|
326
|
+
if (verdict === "added") this.log(`CA de développement ajoutée aux ancres TLS du processus (${this.caPath})`, "DEBUG");
|
|
327
|
+
else if (verdict === "unsupported") this.log(`Ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5) : les requêtes sortantes vers un service qui sert le certificat de développement échoueront. Relancer avec NODE_EXTRA_CA_CERTS=${this.caPath}`, "WARNING");
|
|
199
328
|
}
|
|
200
329
|
/**
|
|
201
330
|
* Résout la stratégie effective à partir de `certificates.strategy` :
|
|
@@ -249,7 +378,7 @@ var Certificate = class Certificate extends Service {
|
|
|
249
378
|
/**
|
|
250
379
|
* SAN effectif : config explicite si fournie, sinon dérivé du kernel
|
|
251
380
|
* (localhost + domain en DNS ; loopback en IP). Une IP littérale (ex. domain
|
|
252
|
-
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC
|
|
381
|
+
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 9525).
|
|
253
382
|
*/
|
|
254
383
|
derivedSan() {
|
|
255
384
|
const san = this.certOptions.san;
|
|
@@ -329,7 +458,7 @@ var Certificate = class Certificate extends Service {
|
|
|
329
458
|
return false;
|
|
330
459
|
}
|
|
331
460
|
}
|
|
332
|
-
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC
|
|
461
|
+
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 9525) ? */
|
|
333
462
|
sanCovers(presentDns) {
|
|
334
463
|
return this.sanDnsNames().every((name) => presentDns.includes(name));
|
|
335
464
|
}
|
|
@@ -351,11 +480,7 @@ var Certificate = class Certificate extends Service {
|
|
|
351
480
|
return Buffer.from(await fsp.readFile(value, "utf8"));
|
|
352
481
|
}
|
|
353
482
|
createFullChain() {
|
|
354
|
-
return
|
|
355
|
-
this.cert,
|
|
356
|
-
this.intermediateCertPem,
|
|
357
|
-
this.rootCertPem
|
|
358
|
-
].map((part) => part?.toString().trim()).filter(Boolean).join("\n");
|
|
483
|
+
return this.cert?.toString().trim() ?? "";
|
|
359
484
|
}
|
|
360
485
|
async readCerticates() {
|
|
361
486
|
for (const file of this.files) try {
|
|
@@ -510,4 +635,4 @@ var Certificate = class Certificate extends Service {
|
|
|
510
635
|
}
|
|
511
636
|
};
|
|
512
637
|
//#endregion
|
|
513
|
-
export { Certificate as default };
|
|
638
|
+
export { addDefaultCaCertificate, Certificate as default, splitPemCertificates, trustCaFile, trustOutboundAuthorities };
|
|
@@ -348,7 +348,7 @@ const SCHEMA_MISMATCH_HINT = {
|
|
|
348
348
|
* reason: error.message
|
|
349
349
|
*
|
|
350
350
|
* Stateless singleton — zero per-request allocation. Override via
|
|
351
|
-
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC
|
|
351
|
+
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 9457.
|
|
352
352
|
*/
|
|
353
353
|
var DefaultErrorRenderer = class {
|
|
354
354
|
renderHttp(error, context) {
|