@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.2
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 +12 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -2
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +26 -4
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +87 -144
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +22 -15
- package/dist/nodefony/service/trace.js +3 -2
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +108 -46
- package/dist/nodefony/src/context/http/Response.js +134 -54
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +65 -23
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +23 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +42 -57
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/index.md +4 -2
- package/docs/observabilite.md +27 -27
- package/docs/rate-limit.md +20 -20
- package/docs/servers.md +82 -79
- package/docs/session.md +74 -69
- package/docs/upload.md +49 -43
- package/package.json +9 -12
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { asServersConfig } from "../src/servers/kernelServers.js";
|
|
2
|
+
import { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid } from "./x509.js";
|
|
1
3
|
import { Service, extend } from "nodefony";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { X509Certificate, generateKeyPair, randomBytes } from "node:crypto";
|
|
3
5
|
import fsp from "node:fs/promises";
|
|
4
6
|
import path, { resolve } from "node:path";
|
|
5
7
|
import { promisify } from "node:util";
|
|
6
8
|
import { execFile } from "node:child_process";
|
|
7
9
|
//#region nodefony/service/certificates.ts
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
+
const generateKeyPairAsync = promisify(generateKeyPair);
|
|
9
12
|
/**
|
|
10
13
|
* Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
|
|
11
14
|
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 6125).
|
|
@@ -35,7 +38,7 @@ const defaultOptions = {
|
|
|
35
38
|
*
|
|
36
39
|
* Trois stratégies : `explicit` (certificat fourni en config — le cas de
|
|
37
40
|
* PRODUCTION), `mkcert` (CA locale trustée, confort de dev) et `selfsigned`
|
|
38
|
-
* (auto-
|
|
41
|
+
* (auto-signé, secours). La génération est réservée au
|
|
39
42
|
* DÉVELOPPEMENT : en production sans certificat fourni, le service crie un
|
|
40
43
|
* avertissement (Nodefony n'est pas une autorité de certification).
|
|
41
44
|
*
|
|
@@ -48,7 +51,6 @@ var Certificate = class Certificate extends Service {
|
|
|
48
51
|
module;
|
|
49
52
|
files = [];
|
|
50
53
|
keysPair = null;
|
|
51
|
-
certForge = null;
|
|
52
54
|
ca = "";
|
|
53
55
|
key = "";
|
|
54
56
|
cert = "";
|
|
@@ -58,13 +60,6 @@ var Certificate = class Certificate extends Service {
|
|
|
58
60
|
intermediateCertPem = "";
|
|
59
61
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
60
62
|
mkcertCaRoot = null;
|
|
61
|
-
/**
|
|
62
|
-
* Backend crypto node-forge, chargé PARESSEUSEMENT. La génération de
|
|
63
|
-
* certificat est un confort de DÉVELOPPEMENT : en production avec un
|
|
64
|
-
* certificat fourni (`strategy: "explicit"`), cette grosse dépendance n'est
|
|
65
|
-
* jamais importée (règle perf-mémoire — pas de dep chargée « au cas où »).
|
|
66
|
-
*/
|
|
67
|
-
forge = null;
|
|
68
63
|
path = resolve(".", "nodefony", "config", "certificates");
|
|
69
64
|
serverPath = resolve(this.path, "server");
|
|
70
65
|
caPath = resolve(this.path, "ca", "nodefony-root-ca.crt.pem");
|
|
@@ -85,20 +80,6 @@ var Certificate = class Certificate extends Service {
|
|
|
85
80
|
return this.kernel?.environment === "development";
|
|
86
81
|
}
|
|
87
82
|
/**
|
|
88
|
-
* Charge node-forge à la demande (idempotent) — uniquement sur le chemin de
|
|
89
|
-
* GÉNÉRATION (selfsigned / mkcert / inspection). Le chemin `explicit` (prod)
|
|
90
|
-
* ne l'appelle jamais.
|
|
91
|
-
*/
|
|
92
|
-
async loadForge() {
|
|
93
|
-
if (!this.forge) this.forge = (await import("node-forge")).default;
|
|
94
|
-
return this.forge;
|
|
95
|
-
}
|
|
96
|
-
/** Accès au backend forge déjà chargé (lève si `loadForge` n'a pas été appelé). */
|
|
97
|
-
get forgeLib() {
|
|
98
|
-
if (!this.forge) throw new Error("node-forge non chargé — appeler loadForge() avant toute génération.");
|
|
99
|
-
return this.forge;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
83
|
* Fabrique-t-on un certificat au démarrage ?
|
|
103
84
|
*
|
|
104
85
|
* 🔴 Seulement si un serveur TLS est ACTIF. Sans cette question, le hook
|
|
@@ -120,7 +101,7 @@ var Certificate = class Certificate extends Service {
|
|
|
120
101
|
* exemplaire. La promesse était écrite ; rien ne la tenait.
|
|
121
102
|
*/
|
|
122
103
|
get tlsWanted() {
|
|
123
|
-
return !!this.module.kernel?.options
|
|
104
|
+
return !!asServersConfig(this.module.kernel?.options.servers)?.https;
|
|
124
105
|
}
|
|
125
106
|
async init() {
|
|
126
107
|
if (!this.tlsWanted) return this;
|
|
@@ -138,8 +119,8 @@ var Certificate = class Certificate extends Service {
|
|
|
138
119
|
*/
|
|
139
120
|
static generateSerialHex() {
|
|
140
121
|
const bytes = randomBytes(16);
|
|
141
|
-
bytes
|
|
142
|
-
|
|
122
|
+
const head = bytes.readUInt8(0) & 127;
|
|
123
|
+
bytes[0] = head === 0 ? 1 : head;
|
|
143
124
|
return bytes.toString("hex");
|
|
144
125
|
}
|
|
145
126
|
setFiles() {
|
|
@@ -179,7 +160,7 @@ var Certificate = class Certificate extends Service {
|
|
|
179
160
|
path.dirname(this.certPath),
|
|
180
161
|
path.dirname(this.fullchainPath)
|
|
181
162
|
];
|
|
182
|
-
for
|
|
163
|
+
for (const directory of directories) try {
|
|
183
164
|
await fsp.access(directory);
|
|
184
165
|
} catch {
|
|
185
166
|
await fsp.mkdir(directory, { recursive: true });
|
|
@@ -189,7 +170,7 @@ var Certificate = class Certificate extends Service {
|
|
|
189
170
|
/**
|
|
190
171
|
* Génère (ou recharge) le certificat serveur selon la stratégie résolue :
|
|
191
172
|
* `explicit` (fourni en config / prod), `mkcert` (dev, CA trustée) ou
|
|
192
|
-
* `selfsigned` (auto-
|
|
173
|
+
* `selfsigned` (auto-signé, secours). Régénère automatiquement si
|
|
193
174
|
* le certificat présent sur disque n'est pas adéquat pour la stratégie active
|
|
194
175
|
* (expiré, SHA-1, SAN incomplet).
|
|
195
176
|
*
|
|
@@ -200,17 +181,13 @@ var Certificate = class Certificate extends Service {
|
|
|
200
181
|
await this.ensureDirectoriesExist();
|
|
201
182
|
const strategy = await this.resolveStrategy();
|
|
202
183
|
if (strategy === "explicit") return this.loadExplicitCert();
|
|
203
|
-
await this.loadForge();
|
|
204
184
|
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) return this.readCerticates();
|
|
205
185
|
if (strategy === "mkcert") await this.generateWithMkcert();
|
|
206
186
|
else {
|
|
207
|
-
this.keysPair = this.generateKeys();
|
|
208
|
-
this.certForge = this.createCertificate();
|
|
209
|
-
this.setExtension();
|
|
210
|
-
this.sign();
|
|
187
|
+
this.keysPair = await this.generateKeys();
|
|
211
188
|
this.key = this.generatePrivateKeyPem();
|
|
212
189
|
this.publicKeyPem = this.generatePublickeyPem();
|
|
213
|
-
const certPem = this.
|
|
190
|
+
const certPem = await this.createCertificate();
|
|
214
191
|
this.cert = certPem;
|
|
215
192
|
this.ca = certPem;
|
|
216
193
|
this.fullchainPem = this.createFullChain();
|
|
@@ -226,7 +203,7 @@ var Certificate = class Certificate extends Service {
|
|
|
226
203
|
* erreur si absents.
|
|
227
204
|
* - `mkcert` : dev + binaire mkcert + CA locale présents. Forcé hors dev →
|
|
228
205
|
* retombe sur `selfsigned` avec avertissement.
|
|
229
|
-
* - `selfsigned` : auto-signé
|
|
206
|
+
* - `selfsigned` : auto-signé (fallback). En PRODUCTION, crie un
|
|
230
207
|
* avertissement : la génération n'est pas le rôle d'un serveur de prod.
|
|
231
208
|
*/
|
|
232
209
|
async resolveStrategy() {
|
|
@@ -237,13 +214,13 @@ var Certificate = class Certificate extends Service {
|
|
|
237
214
|
}
|
|
238
215
|
if (requested === "auto" && this.hasExplicitCert()) return "explicit";
|
|
239
216
|
if (requested === "mkcert" || requested === "auto") {
|
|
240
|
-
if (this.isDev() && this.certOptions.dev
|
|
217
|
+
if (this.isDev() && this.certOptions.dev.useMkcert !== false) {
|
|
241
218
|
const caRoot = await this.detectMkcert();
|
|
242
219
|
if (caRoot) {
|
|
243
220
|
this.mkcertCaRoot = caRoot;
|
|
244
221
|
return "mkcert";
|
|
245
222
|
}
|
|
246
|
-
this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé
|
|
223
|
+
this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé (non trusté). `brew install mkcert nss && mkcert -install` pour un HTTPS dev sans erreur (HMR cross-origin/WSS).", "WARNING");
|
|
247
224
|
} else if (requested === "mkcert") this.log("strategy='mkcert' ignoré hors development → certificat auto-signé.", "WARNING");
|
|
248
225
|
}
|
|
249
226
|
if (!this.isDev()) this.log("Aucun certificat TLS fourni en PRODUCTION : génération d'un auto-signé NON trusté (secours). Nodefony n'est PAS une autorité de certification de production — fournissez un vrai certificat (Let's Encrypt, ingress k8s, reverse-proxy edge) via certificates.{ key, cert, ca } (strategy='explicit').", "WARNING");
|
|
@@ -320,8 +297,10 @@ var Certificate = class Certificate extends Service {
|
|
|
320
297
|
await this.restrictPrivateKey();
|
|
321
298
|
const rootCaPem = await fsp.readFile(path.join(caRoot, "rootCA.pem"), "utf8");
|
|
322
299
|
const certPem = await fsp.readFile(this.certPath, "utf8");
|
|
323
|
-
const
|
|
324
|
-
|
|
300
|
+
const publicKeyPem = new X509Certificate(certPem).publicKey.export({
|
|
301
|
+
type: "spki",
|
|
302
|
+
format: "pem"
|
|
303
|
+
});
|
|
325
304
|
await fsp.writeFile(this.publicKeyPath, publicKeyPem, "utf8");
|
|
326
305
|
await fsp.writeFile(this.fullchainPath, `${certPem}${rootCaPem}`, "utf8");
|
|
327
306
|
await fsp.writeFile(this.caPath, rootCaPem, "utf8");
|
|
@@ -338,25 +317,21 @@ var Certificate = class Certificate extends Service {
|
|
|
338
317
|
*/
|
|
339
318
|
async isCertAdequate(strategy) {
|
|
340
319
|
try {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (cert.validity.notAfter.getTime() <= Date.now()) return false;
|
|
345
|
-
const ext = cert.getExtension("subjectAltName");
|
|
346
|
-
if (!ext || !this.sanCovers(ext.altNames ?? [])) return false;
|
|
320
|
+
const cert = new X509Certificate(await fsp.readFile(this.certPath));
|
|
321
|
+
if (cert.validToDate.getTime() <= Date.now()) return false;
|
|
322
|
+
if (!this.sanCovers(parseSubjectAltName(cert.subjectAltName).dns)) return false;
|
|
347
323
|
if (strategy === "mkcert") {
|
|
348
|
-
const org = cert.issuer
|
|
349
|
-
return
|
|
324
|
+
const org = distinguishedNameField(cert.issuer, "O");
|
|
325
|
+
return org !== null && /mkcert/i.test(org);
|
|
350
326
|
}
|
|
351
|
-
return cert.
|
|
327
|
+
return signatureAlgorithmOid(cert.raw) !== SHA1_WITH_RSA_OID;
|
|
352
328
|
} catch {
|
|
353
329
|
return false;
|
|
354
330
|
}
|
|
355
331
|
}
|
|
356
332
|
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 6125) ? */
|
|
357
|
-
sanCovers(
|
|
358
|
-
|
|
359
|
-
return this.sanDnsNames().every((name) => presentDns.has(name));
|
|
333
|
+
sanCovers(presentDns) {
|
|
334
|
+
return this.sanDnsNames().every((name) => presentDns.includes(name));
|
|
360
335
|
}
|
|
361
336
|
/** Charge un certificat fourni en config (chemin fichier ou Buffer). */
|
|
362
337
|
async loadExplicitCert() {
|
|
@@ -401,7 +376,7 @@ var Certificate = class Certificate extends Service {
|
|
|
401
376
|
}
|
|
402
377
|
async writeCertificates(force = false) {
|
|
403
378
|
await this.ensureDirectoriesExist();
|
|
404
|
-
for
|
|
379
|
+
for (const file of this.files) try {
|
|
405
380
|
if (file.variable) {
|
|
406
381
|
const mode = file.path === this.privateKeyPath ? this.certOptions.privateKeyMode ?? 384 : 420;
|
|
407
382
|
if (force) await fsp.rm(file.path, { force: true });
|
|
@@ -442,86 +417,58 @@ var Certificate = class Certificate extends Service {
|
|
|
442
417
|
this.log(err, "DEBUG");
|
|
443
418
|
}
|
|
444
419
|
}
|
|
445
|
-
|
|
446
|
-
|
|
420
|
+
/** Génère la paire RSA (taille configurée) hors de la boucle d'événements. */
|
|
421
|
+
async generateKeys() {
|
|
422
|
+
return generateKeyPairAsync("rsa", { modulusLength: this.certOptions.selfSigned.size });
|
|
447
423
|
}
|
|
424
|
+
/** Clé privée en PEM PKCS#1 (`BEGIN RSA PRIVATE KEY`). */
|
|
448
425
|
generatePrivateKeyPem() {
|
|
449
|
-
if (this.keysPair) return Buffer.from(this.
|
|
450
|
-
|
|
426
|
+
if (this.keysPair) return Buffer.from(this.keysPair.privateKey.export({
|
|
427
|
+
type: "pkcs1",
|
|
428
|
+
format: "pem"
|
|
429
|
+
}));
|
|
430
|
+
throw new Error(`KeyPair not found`);
|
|
451
431
|
}
|
|
432
|
+
/** Clé publique en PEM SPKI (`BEGIN PUBLIC KEY`). */
|
|
452
433
|
generatePublickeyPem() {
|
|
453
|
-
if (this.keysPair) return Buffer.from(this.
|
|
454
|
-
|
|
434
|
+
if (this.keysPair) return Buffer.from(this.keysPair.publicKey.export({
|
|
435
|
+
type: "spki",
|
|
436
|
+
format: "pem"
|
|
437
|
+
}));
|
|
438
|
+
throw new Error(`KeyPair not found`);
|
|
455
439
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Fabrique et signe le certificat auto-signé avec la paire courante.
|
|
442
|
+
*
|
|
443
|
+
* Série aléatoire unique (RFC 5280 §4.1.2.2), `notBefore` reculé (décalage
|
|
444
|
+
* d'horloge du client), SAN DNS + IP (config explicite sinon dérivé), signé
|
|
445
|
+
* avec le hachage configuré — jamais SHA-1 (collision SHAttered 2017,
|
|
446
|
+
* CA/Browser Forum depuis 2016).
|
|
447
|
+
*
|
|
448
|
+
* @returns le certificat en PEM
|
|
449
|
+
* @throws Si la paire de clés n'a pas été générée
|
|
450
|
+
*/
|
|
451
|
+
async createCertificate() {
|
|
461
452
|
if (!this.keysPair) throw new Error(`KeyPair not found`);
|
|
462
453
|
const o = this.certOptions.selfSigned;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
/** subjectAltName de l'auto-signé : DNS + IP (config explicite sinon dérivé). */
|
|
475
|
-
altNames() {
|
|
476
|
-
const out = [];
|
|
477
|
-
for (const dns of this.sanDnsNames()) out.push({
|
|
478
|
-
type: 2,
|
|
479
|
-
value: dns
|
|
480
|
-
});
|
|
481
|
-
for (const ip of this.sanIps()) out.push({
|
|
482
|
-
type: 7,
|
|
483
|
-
ip
|
|
454
|
+
const start = Date.now() - o.backdateMinutes * 6e4;
|
|
455
|
+
const pem = await createSelfSignedCertificate({
|
|
456
|
+
privateKey: this.keysPair.privateKey,
|
|
457
|
+
publicKey: this.keysPair.publicKey,
|
|
458
|
+
serialHex: Certificate.generateSerialHex(),
|
|
459
|
+
notBefore: new Date(start),
|
|
460
|
+
notAfter: new Date(start + o.validityDays * 864e5),
|
|
461
|
+
attributes: o.attrs,
|
|
462
|
+
dns: this.sanDnsNames(),
|
|
463
|
+
ip: this.sanIps(),
|
|
464
|
+
hash: this.allowedHash(o.hash)
|
|
484
465
|
});
|
|
485
|
-
return
|
|
486
|
-
}
|
|
487
|
-
setExtension() {
|
|
488
|
-
if (!this.certForge) throw new Error(`pki.Certificate not found`);
|
|
489
|
-
this.certForge.setExtensions([
|
|
490
|
-
{
|
|
491
|
-
name: "basicConstraints",
|
|
492
|
-
cA: false
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
name: "keyUsage",
|
|
496
|
-
digitalSignature: true,
|
|
497
|
-
keyEncipherment: true
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
name: "extKeyUsage",
|
|
501
|
-
serverAuth: true,
|
|
502
|
-
clientAuth: true
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
name: "subjectAltName",
|
|
506
|
-
altNames: this.altNames()
|
|
507
|
-
},
|
|
508
|
-
{ name: "subjectKeyIdentifier" }
|
|
509
|
-
]);
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Signe le certificat avec le hachage configuré (SHA-256 par défaut).
|
|
513
|
-
* SHA-1 est INTERDIT (collision SHAttered 2017, CA/Browser Forum depuis 2016) :
|
|
514
|
-
* `node-forge` signe en SHA-1 par défaut si on ne passe pas de digest → on en
|
|
515
|
-
* passe toujours un.
|
|
516
|
-
*/
|
|
517
|
-
sign() {
|
|
518
|
-
if (!this.certForge || !this.keysPair) throw new Error(`pki.rsa.KeyPair or pki.Certificate not found`);
|
|
519
|
-
this.certForge.sign(this.keysPair.privateKey, this.digestFor(this.certOptions.selfSigned.hash));
|
|
466
|
+
return Buffer.from(pem);
|
|
520
467
|
}
|
|
521
468
|
/**
|
|
522
469
|
* Résumé introspectable du certificat serveur courant — réutilisé par la
|
|
523
470
|
* commande CLI `certificates` et un futur endpoint d'admin Studio (parité
|
|
524
|
-
* CLI ↔ Web).
|
|
471
|
+
* CLI ↔ Web).
|
|
525
472
|
*/
|
|
526
473
|
async describe() {
|
|
527
474
|
const info = {
|
|
@@ -535,34 +482,30 @@ var Certificate = class Certificate extends Service {
|
|
|
535
482
|
info.caPath = this.caPath;
|
|
536
483
|
} catch {}
|
|
537
484
|
if (!this.cert) return info;
|
|
538
|
-
const { pki } = await this.loadForge();
|
|
539
485
|
try {
|
|
540
|
-
const cert =
|
|
541
|
-
info.serial = cert.serialNumber;
|
|
542
|
-
info.validFrom = cert.
|
|
543
|
-
info.validTo = cert.
|
|
544
|
-
info.signatureAlgorithm =
|
|
545
|
-
const cn = cert.subject.
|
|
546
|
-
if (cn) info.commonName =
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
486
|
+
const cert = new X509Certificate(this.cert);
|
|
487
|
+
info.serial = cert.serialNumber.toLowerCase();
|
|
488
|
+
info.validFrom = cert.validFromDate.toISOString();
|
|
489
|
+
info.validTo = cert.validToDate.toISOString();
|
|
490
|
+
info.signatureAlgorithm = signatureAlgorithmName(signatureAlgorithmOid(cert.raw));
|
|
491
|
+
const cn = cert.subject ? distinguishedNameField(cert.subject, "CN") : null;
|
|
492
|
+
if (cn !== null) info.commonName = cn;
|
|
493
|
+
const san = parseSubjectAltName(cert.subjectAltName);
|
|
494
|
+
if (san.dns.length > 0 || san.ip.length > 0) info.san = [...san.dns, ...san.ip];
|
|
549
495
|
} catch {}
|
|
550
496
|
return info;
|
|
551
497
|
}
|
|
552
|
-
/**
|
|
553
|
-
|
|
554
|
-
return pki.oids[oid] ?? oid;
|
|
555
|
-
}
|
|
556
|
-
/** Digest node-forge correspondant au hachage configuré (jamais SHA-1). */
|
|
557
|
-
digestFor(hash) {
|
|
558
|
-
const { md } = this.forgeLib;
|
|
498
|
+
/** Hachage configuré, ramené à SHA-256 s'il sort du contrat (jamais SHA-1). */
|
|
499
|
+
allowedHash(hash) {
|
|
559
500
|
switch (hash) {
|
|
560
|
-
case "sha512":
|
|
561
|
-
case "sha384":
|
|
562
|
-
case "sha256": return
|
|
563
|
-
default:
|
|
564
|
-
|
|
565
|
-
|
|
501
|
+
case "sha512":
|
|
502
|
+
case "sha384":
|
|
503
|
+
case "sha256": return hash;
|
|
504
|
+
default: {
|
|
505
|
+
const refused = hash;
|
|
506
|
+
this.log(`Hachage '${refused}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
|
|
507
|
+
return "sha256";
|
|
508
|
+
}
|
|
566
509
|
}
|
|
567
510
|
}
|
|
568
511
|
};
|
|
@@ -111,7 +111,7 @@ function toValidationFields(error) {
|
|
|
111
111
|
function toValidationError(error, fields) {
|
|
112
112
|
const summary = fields.map((f) => f.field ? `${f.field}: ${f.message}` : f.message).join(" · ");
|
|
113
113
|
const validation = new nodefonyError(`Validation failed — ${summary}`, VALIDATION_STATUS);
|
|
114
|
-
validation.stack = error.stack;
|
|
114
|
+
if (error.stack !== void 0) validation.stack = error.stack;
|
|
115
115
|
validation.fields = fields;
|
|
116
116
|
return validation;
|
|
117
117
|
}
|
|
@@ -206,8 +206,11 @@ function malformedIdentifierOf(error) {
|
|
|
206
206
|
const candidate = current;
|
|
207
207
|
if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
|
|
208
208
|
if (String(candidate.code) === "22P02") {
|
|
209
|
-
const match = PG_INVALID_INPUT.exec(candidate.message
|
|
210
|
-
if (match
|
|
209
|
+
const match = PG_INVALID_INPUT.exec(candidate.message);
|
|
210
|
+
if (match) {
|
|
211
|
+
const [, type = "", value = null] = match;
|
|
212
|
+
if (IDENTIFIER_COLUMN_TYPES.has(type)) return value;
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
215
|
current = candidate.cause;
|
|
213
216
|
}
|
|
@@ -307,7 +310,7 @@ function schemaMismatchOf(error) {
|
|
|
307
310
|
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
308
311
|
const candidate = current;
|
|
309
312
|
if (candidate.code !== void 0 && SCHEMA_MISMATCH_CODES.has(String(candidate.code)) || candidate.errno !== void 0 && SCHEMA_MISMATCH_CODES.has(String(candidate.errno))) return "certain";
|
|
310
|
-
if (String(candidate.code) === "SQLITE_ERROR" && SQLITE_SCHEMA_PREFIXES.some((p) => candidate.message
|
|
313
|
+
if (String(candidate.code) === "SQLITE_ERROR" && SQLITE_SCHEMA_PREFIXES.some((p) => candidate.message.startsWith(p))) return "probable";
|
|
311
314
|
current = candidate.cause;
|
|
312
315
|
}
|
|
313
316
|
return null;
|
|
@@ -373,7 +376,7 @@ var DefaultErrorRenderer = class {
|
|
|
373
376
|
const httpError = this.toHttpError(error, context);
|
|
374
377
|
const rawCode = httpError.code;
|
|
375
378
|
let code = typeof rawCode === "number" && Number.isInteger(rawCode) ? rawCode : 500;
|
|
376
|
-
if (context
|
|
379
|
+
if (context.rejected === false) code = toWsCloseCode(code);
|
|
377
380
|
else if (code > 599) code = 500;
|
|
378
381
|
const internal = code === 1011 || code >= 500 && code <= 599;
|
|
379
382
|
const reason = isProduction() && internal ? OPAQUE_SERVER_ERROR : httpError.message;
|
|
@@ -392,20 +395,20 @@ var DefaultErrorRenderer = class {
|
|
|
392
395
|
}
|
|
393
396
|
if (isUniqueViolation(error)) {
|
|
394
397
|
const conflict = new nodefonyError(UNIQUE_VIOLATION_MESSAGE, CONFLICT_STATUS);
|
|
395
|
-
conflict.stack = error.stack;
|
|
398
|
+
if (error.stack !== void 0) conflict.stack = error.stack;
|
|
396
399
|
return new HttpError(conflict, CONFLICT_STATUS, context);
|
|
397
400
|
}
|
|
398
401
|
const referencedBy = referencedByOf(error);
|
|
399
402
|
if (referencedBy !== null) {
|
|
400
403
|
const conflict = new nodefonyError(`Conflict — this resource is still referenced by ${referencedBy}`, CONFLICT_STATUS);
|
|
401
|
-
conflict.stack = error.stack;
|
|
404
|
+
if (error.stack !== void 0) conflict.stack = error.stack;
|
|
402
405
|
return new HttpError(conflict, CONFLICT_STATUS, context);
|
|
403
406
|
}
|
|
404
407
|
const malformed = malformedIdentifierOf(error);
|
|
405
408
|
if (malformed !== null) {
|
|
406
409
|
const status = malformedIdentifierStatus(malformed, context?.url);
|
|
407
410
|
const refused = new nodefonyError(status === 404 ? "Not Found — this identifier cannot designate any resource" : "Bad Request — malformed identifier", status);
|
|
408
|
-
refused.stack = error.stack;
|
|
411
|
+
if (error.stack !== void 0) refused.stack = error.stack;
|
|
409
412
|
return new HttpError(refused, status, context);
|
|
410
413
|
}
|
|
411
414
|
const code = error.code;
|