@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- 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 +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,17 +1,209 @@
|
|
|
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
|
+
// 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
|
+
* Chaque refus dit le problème, d'où vient le chemin quand on le sait, puis le
|
|
70
|
+
* geste qui répare ({@link caFileRemedy}).
|
|
71
|
+
*
|
|
72
|
+
* @param store - accès aux ancres ; `node:tls` par défaut
|
|
73
|
+
* @param origin - phrase qui dit d'où vient le chemin (` Valeur venue de …`), insérée dans un refus
|
|
74
|
+
* @returns le bilan de l'ajout
|
|
75
|
+
* @throws BootConfigurationError - fichier illisible, sans certificat, ou Node
|
|
76
|
+
* incapable d'élargir ses ancres en cours d'exécution
|
|
77
|
+
*/
|
|
78
|
+
async function trustCaFile(file, root, store = tls, origin = "") {
|
|
79
|
+
const absolute = path.resolve(root, file);
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = await fsp.readFile(absolute, "utf8");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
85
|
+
const reason = typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
86
|
+
throw new BootConfigurationError(`${unreadableProblem(absolute, reason)}${origin} ${caFileRemedy(reason)}`);
|
|
87
|
+
}
|
|
88
|
+
const certificates = splitPemCertificates(content);
|
|
89
|
+
if (certificates.length === 0) throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » ne contient aucun certificat PEM (-----BEGIN CERTIFICATE-----).${origin} ${caFileRemedy("NOPEM")}`);
|
|
90
|
+
let added = 0;
|
|
91
|
+
let present = 0;
|
|
92
|
+
for (const pem of certificates) {
|
|
93
|
+
const verdict = addDefaultCaCertificate(pem, store);
|
|
94
|
+
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}.`);
|
|
95
|
+
if (verdict === "added") added += 1;
|
|
96
|
+
else present += 1;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
file: absolute,
|
|
100
|
+
added,
|
|
101
|
+
present
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Première phrase d'un refus de lecture : le fichier et la cause en un mot. */
|
|
105
|
+
function unreadableProblem(absolute, reason) {
|
|
106
|
+
return `http.certificates.extraCaFiles : « ${absolute} » est illisible (${reason}).`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Le geste qui répare une autorité déclarée qu'on ne peut pas reconnaître.
|
|
110
|
+
*
|
|
111
|
+
* Un refus au démarrage qui ne dit que la cause envoie chercher ; celui-ci dit
|
|
112
|
+
* quoi faire. Pure : éprouvée cause par cause, y compris celles qu'une machine
|
|
113
|
+
* de test ne sait pas produire (un droit refusé ne l'est pas à `root`, ni
|
|
114
|
+
* sous Windows).
|
|
115
|
+
*
|
|
116
|
+
* @param cause - code système (`ENOENT`, `EACCES`, `EPERM`, `EISDIR`),
|
|
117
|
+
* `NOPEM` pour un fichier lu sans certificat, ou tout autre motif
|
|
118
|
+
* @returns la phrase du geste
|
|
119
|
+
*/
|
|
120
|
+
function caFileRemedy(cause) {
|
|
121
|
+
switch (cause) {
|
|
122
|
+
case "ENOENT": return "Poser le fichier à ce chemin (absolu, ou relatif à la racine de l'application), ou retirer la déclaration.";
|
|
123
|
+
case "EACCES":
|
|
124
|
+
case "EPERM": return "Le compte qui lance l'application n'a pas le droit de le lire : lui donner ce droit (sous Kubernetes, monter le ConfigMap ou le Secret lisible par l'utilisateur du pod).";
|
|
125
|
+
case "EISDIR": return "C'est un dossier : désigner le fichier PEM qu'il contient.";
|
|
126
|
+
case "NOPEM": return "Le fichier doit porter le CERTIFICAT de l'autorité (pas sa clé privée), au format PEM ; un fichier DER se convertit par `openssl x509 -inform der -in autorite.der -out autorite.pem`.";
|
|
127
|
+
default: return "Vérifier le chemin et les droits du fichier, ou retirer la déclaration.";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Dossier où Nodefony range les certificats qu'il fabrique en développement,
|
|
132
|
+
* relatif au répertoire courant.
|
|
133
|
+
*
|
|
134
|
+
* @returns le chemin absolu de `nodefony/config/certificates`
|
|
135
|
+
*/
|
|
136
|
+
function certificatesDirectory() {
|
|
137
|
+
return resolve(".", "nodefony", "config", "certificates");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Chemin de l'autorité de DÉVELOPPEMENT : le certificat racine que Nodefony
|
|
141
|
+
* fabrique lui-même pour signer le certificat du serveur local. Généré, jamais
|
|
142
|
+
* versionné — une seule convention, que le service et la confiance sortante lisent.
|
|
143
|
+
*
|
|
144
|
+
* @param directory - dossier des certificats ; {@link certificatesDirectory} par défaut
|
|
145
|
+
* @returns le chemin absolu du fichier PEM de l'autorité
|
|
146
|
+
*/
|
|
147
|
+
function developmentAuthorityPath(directory = certificatesDirectory()) {
|
|
148
|
+
return resolve(directory, "ca", "nodefony-root-ca.crt.pem");
|
|
149
|
+
}
|
|
150
|
+
/** Vrai si le fichier n'existe pas (et seulement dans ce cas). */
|
|
151
|
+
async function isMissing(file) {
|
|
152
|
+
try {
|
|
153
|
+
await fsp.stat(file);
|
|
154
|
+
return false;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Ajoute aux ancres TLS du processus les autorités déclarées
|
|
161
|
+
* (`certificates.extraCaFiles`), au plus tôt : appelé à l'enregistrement du
|
|
162
|
+
* module, avant tout service — toute requête sortante, commande console
|
|
163
|
+
* comprise, les trouve en place.
|
|
164
|
+
*
|
|
165
|
+
* L'autorité de DÉVELOPPEMENT déclarée mais pas encore engendrée est ignorée en
|
|
166
|
+
* développement, et le journal le dit : elle n'a signé aucun certificat, il n'y
|
|
167
|
+
* a donc rien à lui faire confiance, et le serveur de développement la pose
|
|
168
|
+
* lui-même quand il la fabrique ({@link Certificate.generateServerCertificates}).
|
|
169
|
+
* Rien n'est fabriqué ici : une commande console n'écrit pas de certificat.
|
|
170
|
+
* Une autre autorité absente, ou n'importe laquelle hors développement, reste
|
|
171
|
+
* un refus — qui nomme la variable d'origine et le geste qui répare
|
|
172
|
+
* ({@link caFileRemedy}).
|
|
173
|
+
*
|
|
174
|
+
* @param files - `certificates.extraCaFiles`
|
|
175
|
+
* @param root - racine de l'application
|
|
176
|
+
* @param log - journal du module
|
|
177
|
+
* @param context - décor (environnement, autorité de dev, ancres)
|
|
178
|
+
* @throws BootConfigurationError - cf {@link trustCaFile}
|
|
179
|
+
*/
|
|
180
|
+
async function trustOutboundAuthorities(files, root, log, context = {}) {
|
|
181
|
+
const developmentAuthority = context.developmentAuthority ?? developmentAuthorityPath();
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const absolute = path.resolve(root, file);
|
|
184
|
+
const isDevelopmentAuthority = absolute === developmentAuthority;
|
|
185
|
+
if (isDevelopmentAuthority && context.development === true && await isMissing(absolute)) {
|
|
186
|
+
log(`autorité de développement ${absolute} pas encore engendrée : ignorée. Elle naît au premier \`npx nodefony development\` (ou \`npx nodefony http:certificates\`), et le serveur de développement lui fait alors confiance de lui-même.`, "INFO");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const origin = originOf(absolute, root, context.env ?? process.env);
|
|
190
|
+
if (isDevelopmentAuthority && await isMissing(absolute)) throw new BootConfigurationError(`${unreadableProblem(absolute, "ENOENT")}${origin} C'est l'autorité de DÉVELOPPEMENT : Nodefony la fabrique au premier \`npx nodefony development\`, jamais hors développement. Pour essayer la production sur ce poste, l'engendrer une fois (\`npx nodefony http:certificates\`) puis relancer ; sinon retirer la déclaration — une production réelle n'en a pas besoin.`);
|
|
191
|
+
const trust = await trustCaFile(file, root, context.store, origin);
|
|
192
|
+
log(`autorité sortante ${trust.file} : ${trust.added} ajoutée(s), ${trust.present} déjà présente(s)`, "INFO");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Nomme la variable d'où vient le chemin, quand c'est `NF_TLS_CA_FILE` — la
|
|
197
|
+
* déclaration que pose le gabarit d'application. Constaté sur la valeur, jamais
|
|
198
|
+
* supposé : une configuration écrite à la main n'est pas attribuée à la variable.
|
|
199
|
+
*/
|
|
200
|
+
function originOf(absolute, root, env) {
|
|
201
|
+
const declared = env.NF_TLS_CA_FILE;
|
|
202
|
+
return declared && path.resolve(root, declared) === absolute ? " Valeur venue de NF_TLS_CA_FILE (`.env` ou environnement du processus)." : "";
|
|
203
|
+
}
|
|
12
204
|
/**
|
|
13
205
|
* 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
|
|
206
|
+
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 9525).
|
|
15
207
|
*/
|
|
16
208
|
function isIpLiteral(host) {
|
|
17
209
|
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
|
@@ -44,7 +236,7 @@ const defaultOptions = {
|
|
|
44
236
|
*
|
|
45
237
|
* Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
|
|
46
238
|
* 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
|
|
239
|
+
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 9525), `notBefore` reculé,
|
|
48
240
|
* clé privée écrite en `0600`.
|
|
49
241
|
*/
|
|
50
242
|
var Certificate = class Certificate extends Service {
|
|
@@ -56,13 +248,11 @@ var Certificate = class Certificate extends Service {
|
|
|
56
248
|
cert = "";
|
|
57
249
|
fullchainPem = "";
|
|
58
250
|
publicKeyPem = "";
|
|
59
|
-
rootCertPem = "";
|
|
60
|
-
intermediateCertPem = "";
|
|
61
251
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
62
252
|
mkcertCaRoot = null;
|
|
63
|
-
path =
|
|
253
|
+
path = certificatesDirectory();
|
|
64
254
|
serverPath = resolve(this.path, "server");
|
|
65
|
-
caPath =
|
|
255
|
+
caPath = developmentAuthorityPath(this.path);
|
|
66
256
|
publicKeyPath = resolve(this.path, "server", "publickey.pem");
|
|
67
257
|
privateKeyPath = resolve(this.path, "server", "privkey.pem");
|
|
68
258
|
certPath = resolve(this.path, "server", "cert.pem");
|
|
@@ -181,7 +371,11 @@ var Certificate = class Certificate extends Service {
|
|
|
181
371
|
await this.ensureDirectoriesExist();
|
|
182
372
|
const strategy = await this.resolveStrategy();
|
|
183
373
|
if (strategy === "explicit") return this.loadExplicitCert();
|
|
184
|
-
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy))
|
|
374
|
+
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) {
|
|
375
|
+
await this.readCerticates();
|
|
376
|
+
this.trustDevelopmentAuthority();
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
185
379
|
if (strategy === "mkcert") await this.generateWithMkcert();
|
|
186
380
|
else {
|
|
187
381
|
this.keysPair = await this.generateKeys();
|
|
@@ -195,7 +389,26 @@ var Certificate = class Certificate extends Service {
|
|
|
195
389
|
await this.writeCertificates(true);
|
|
196
390
|
await fsp.writeFile(this.caPath, certPem.toString(), "utf8");
|
|
197
391
|
}
|
|
198
|
-
|
|
392
|
+
await this.readCerticates();
|
|
393
|
+
this.trustDevelopmentAuthority();
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* En développement, fait confiance à la CA que Nodefony vient de générer ou de
|
|
398
|
+
* relire, pour les requêtes SORTANTES du processus.
|
|
399
|
+
*
|
|
400
|
+
* Sans elle, un service du décor qui sert le certificat de l'application
|
|
401
|
+
* (Keycloak, l'auto-vérification d'un jeton MCP) est injoignable depuis
|
|
402
|
+
* `nodefony development` — `fetch failed` / `SELF_SIGNED_CERT_IN_CHAIN` — alors
|
|
403
|
+
* que `curl -k` répond : la confiance n'existait que sous le script de banc.
|
|
404
|
+
* Réservé au DÉVELOPPEMENT et aux certificats que Nodefony fabrique : un
|
|
405
|
+
* certificat `explicit` (production) n'élargit jamais la confiance du processus.
|
|
406
|
+
*/
|
|
407
|
+
trustDevelopmentAuthority() {
|
|
408
|
+
if (!this.isDev() || !this.ca || this.ca.length === 0) return;
|
|
409
|
+
const verdict = addDefaultCaCertificate(this.ca.toString());
|
|
410
|
+
if (verdict === "added") this.log(`CA de développement ajoutée aux ancres TLS du processus (${this.caPath})`, "DEBUG");
|
|
411
|
+
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
412
|
}
|
|
200
413
|
/**
|
|
201
414
|
* Résout la stratégie effective à partir de `certificates.strategy` :
|
|
@@ -249,7 +462,7 @@ var Certificate = class Certificate extends Service {
|
|
|
249
462
|
/**
|
|
250
463
|
* SAN effectif : config explicite si fournie, sinon dérivé du kernel
|
|
251
464
|
* (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
|
|
465
|
+
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 9525).
|
|
253
466
|
*/
|
|
254
467
|
derivedSan() {
|
|
255
468
|
const san = this.certOptions.san;
|
|
@@ -329,7 +542,7 @@ var Certificate = class Certificate extends Service {
|
|
|
329
542
|
return false;
|
|
330
543
|
}
|
|
331
544
|
}
|
|
332
|
-
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC
|
|
545
|
+
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 9525) ? */
|
|
333
546
|
sanCovers(presentDns) {
|
|
334
547
|
return this.sanDnsNames().every((name) => presentDns.includes(name));
|
|
335
548
|
}
|
|
@@ -351,11 +564,8 @@ var Certificate = class Certificate extends Service {
|
|
|
351
564
|
return Buffer.from(await fsp.readFile(value, "utf8"));
|
|
352
565
|
}
|
|
353
566
|
createFullChain() {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
this.intermediateCertPem,
|
|
357
|
-
this.rootCertPem
|
|
358
|
-
].map((part) => part?.toString().trim()).filter(Boolean).join("\n");
|
|
567
|
+
const pem = this.cert?.toString().trim() ?? "";
|
|
568
|
+
return pem === "" ? "" : `${pem}\n`;
|
|
359
569
|
}
|
|
360
570
|
async readCerticates() {
|
|
361
571
|
for (const file of this.files) try {
|
|
@@ -509,5 +719,4 @@ var Certificate = class Certificate extends Service {
|
|
|
509
719
|
}
|
|
510
720
|
}
|
|
511
721
|
};
|
|
512
|
-
|
|
513
|
-
export { Certificate as default };
|
|
722
|
+
export { addDefaultCaCertificate, caFileRemedy, certificatesDirectory, Certificate as default, developmentAuthorityPath, splitPemCertificates, trustCaFile, trustOutboundAuthorities };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import HttpError from "../src/errors/httpError.js";
|
|
2
2
|
import { toWsCloseCode } from "../src/context/websocket/WebsocketContext.js";
|
|
3
3
|
import { Nodefony, nodefonyError } from "nodefony";
|
|
4
|
-
|
|
4
|
+
// nodefony/service/error-renderer.ts
|
|
5
5
|
/** Une donnée rejetée par la validation n'est pas une requête malformée : 422, pas 400. */
|
|
6
6
|
const VALIDATION_STATUS = 422;
|
|
7
7
|
/**
|
|
@@ -74,8 +74,15 @@ const INTERNAL_ERROR_KEYS = [
|
|
|
74
74
|
* deux tests — un gate qu'on ne peut plus voir mordre.
|
|
75
75
|
*/
|
|
76
76
|
const isProduction = () => Nodefony.getKernel()?.environment === "production";
|
|
77
|
+
/** Une propriété d'une anomalie de forme inconnue, lue sans rien supposer. */
|
|
78
|
+
function readIssueProperty(issue, key) {
|
|
79
|
+
return typeof issue === "object" && issue !== null ? Reflect.get(issue, key) : void 0;
|
|
80
|
+
}
|
|
81
|
+
/** Erreurs de validation reconnues : Zod, et celle des routes à schéma (cœur). */
|
|
82
|
+
const VALIDATION_ERROR_NAMES = /* @__PURE__ */ new Set(["ZodError", "SchemaValidationError"]);
|
|
77
83
|
/**
|
|
78
|
-
* Reconnaît une erreur de validation
|
|
84
|
+
* Reconnaît une erreur de validation — `ZodError`, ou `SchemaValidationError`
|
|
85
|
+
* d'une route à schéma — et en extrait les champs fautifs.
|
|
79
86
|
*
|
|
80
87
|
* Reconnaissance **structurelle** (`name` + `issues`), pas `instanceof ZodError` :
|
|
81
88
|
* une application peut embarquer sa propre copie de zod (résolutions npm multiples),
|
|
@@ -88,13 +95,21 @@ const isProduction = () => Nodefony.getKernel()?.environment === "production";
|
|
|
88
95
|
* @returns les champs fautifs, ou `null` si ce n'est pas une erreur de validation.
|
|
89
96
|
*/
|
|
90
97
|
function toValidationFields(error) {
|
|
91
|
-
const
|
|
92
|
-
if (error.name
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
message
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
const issues = Reflect.get(error, "issues");
|
|
99
|
+
if (!VALIDATION_ERROR_NAMES.has(error.name) || !Array.isArray(issues)) return null;
|
|
100
|
+
return issues.map((issue) => {
|
|
101
|
+
const path = readIssueProperty(issue, "path");
|
|
102
|
+
const message = readIssueProperty(issue, "message");
|
|
103
|
+
const code = readIssueProperty(issue, "code");
|
|
104
|
+
const part = readIssueProperty(issue, "in");
|
|
105
|
+
const field = {
|
|
106
|
+
field: Array.isArray(path) ? path.map(String).join(".") : "",
|
|
107
|
+
message: typeof message === "string" ? message : "invalide",
|
|
108
|
+
rule: typeof code === "string" ? code : void 0
|
|
109
|
+
};
|
|
110
|
+
if (typeof part === "string") field.in = part;
|
|
111
|
+
return field;
|
|
112
|
+
});
|
|
98
113
|
}
|
|
99
114
|
/**
|
|
100
115
|
* Construit l'erreur **422** exposée au client à partir des champs fautifs.
|
|
@@ -109,7 +124,10 @@ function toValidationFields(error) {
|
|
|
109
124
|
* reçoit `error.fields` et sait QUEL champ corriger — pas seulement que « ça a échoué ».
|
|
110
125
|
*/
|
|
111
126
|
function toValidationError(error, fields) {
|
|
112
|
-
const summary = fields.map((f) =>
|
|
127
|
+
const summary = fields.map((f) => {
|
|
128
|
+
const where = f.in && f.field ? `${f.in}.${f.field}` : f.in || f.field;
|
|
129
|
+
return where ? `${where}: ${f.message}` : f.message;
|
|
130
|
+
}).join(" · ");
|
|
113
131
|
const validation = new nodefonyError(`Validation failed — ${summary}`, VALIDATION_STATUS);
|
|
114
132
|
if (error.stack !== void 0) validation.stack = error.stack;
|
|
115
133
|
validation.fields = fields;
|
|
@@ -348,7 +366,7 @@ const SCHEMA_MISMATCH_HINT = {
|
|
|
348
366
|
* reason: error.message
|
|
349
367
|
*
|
|
350
368
|
* Stateless singleton — zero per-request allocation. Override via
|
|
351
|
-
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC
|
|
369
|
+
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 9457.
|
|
352
370
|
*/
|
|
353
371
|
var DefaultErrorRenderer = class {
|
|
354
372
|
renderHttp(error, context) {
|
|
@@ -429,5 +447,4 @@ var DefaultErrorRenderer = class {
|
|
|
429
447
|
return code;
|
|
430
448
|
}
|
|
431
449
|
};
|
|
432
|
-
|
|
433
|
-
export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf };
|
|
450
|
+
export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf, toValidationFields };
|