@nodefony/http 10.0.0-alpha.8 → 10.0.0-beta.1
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 +7 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.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 -1
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +22 -1
- 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 +25 -13
- package/dist/nodefony/service/error-renderer.js +118 -5
- 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 +25 -17
- package/dist/nodefony/service/trace.js +1 -1
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- 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 +93 -44
- package/dist/nodefony/src/context/http/Response.js +107 -51
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- 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 +8 -21
- 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 +22 -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 +10 -10
- package/dist/types/nodefony/service/error-renderer.d.ts +14 -0
- 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/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 +6 -19
- 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/observabilite.md +30 -30
- package/docs/rate-limit.md +22 -22
- package/docs/servers.md +61 -61
- package/docs/session.md +82 -77
- package/docs/upload.md +43 -37
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -34,6 +34,13 @@ serveur → http-kernel.handle() → rate-limit IP → createContext()
|
|
|
34
34
|
Chaque contexte (HTTP + WS) porte un `requestId` (UUID v4, ou `X-Request-Id` client),
|
|
35
35
|
réinjecté dans la réponse et corrélé dans les logs via AsyncLocalStorage.
|
|
36
36
|
|
|
37
|
+
Chaque requête HTTP — et chaque connexion WebSocket — reçoit aussi **son propre conteneur de
|
|
38
|
+
services** : un scope ouvert à l'entrée (`enterScope`), dont le prototype est le conteneur du kernel,
|
|
39
|
+
refermé après les hooks `onAfterResponse`/`onFinish` (`leaveScope`), y compris quand un hook lève.
|
|
40
|
+
Le serveur le pose dans le contexte asynchrone de la requête : `RequestContext.getScope()` le rend
|
|
41
|
+
depuis n'importe quel code, et les services `@injectable({ scope: "request" })` y vivent. Détail :
|
|
42
|
+
[Injection & portées](https://github.com/nodefony/nodefony-core/blob/main/docs/architecture/injection-portees.md).
|
|
43
|
+
|
|
37
44
|
## Rate-limit général par IP
|
|
38
45
|
|
|
39
46
|
Plafond de trafic **par IP cliente** sur toutes les routes HTTP — protège la capacité du serveur
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.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.151.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,4 +1,4 @@
|
|
|
1
|
-
import config, { httpConfigSchema } from "./nodefony/config/config.js";
|
|
1
|
+
import config, { httpConfigSchema, httpOverlaySchema } from "./nodefony/config/config.js";
|
|
2
2
|
import { defineHttpConfig, httpConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
3
3
|
import { createHttpAdminApi } from "./nodefony/service/HttpAdminApi.js";
|
|
4
4
|
import { createProfilerAdminApi } from "./nodefony/service/ProfilerAdminApi.js";
|
|
@@ -21,8 +21,8 @@ import DefaultErrorRenderer, { schemaMismatchOf } from "./nodefony/service/error
|
|
|
21
21
|
import DefaultRequestLogger from "./nodefony/service/request-logger.js";
|
|
22
22
|
import JsonAuditLogger from "./nodefony/service/audit-logger.js";
|
|
23
23
|
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.
|
|
24
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
25
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
26
26
|
import http_kernel_default from "./nodefony/service/http-kernel.js";
|
|
27
27
|
import server_http_default from "./nodefony/service/servers/server-http.js";
|
|
28
28
|
import server_https_default from "./nodefony/service/servers/server-https.js";
|
|
@@ -42,6 +42,7 @@ import { PrebuiltUi, resolveUiDelivery } from "./nodefony/src/assets/prebuiltUi.
|
|
|
42
42
|
import { Kernel, Module, services } from "nodefony";
|
|
43
43
|
//#region index.ts
|
|
44
44
|
let Http = class Http extends Module {
|
|
45
|
+
overlaySchema = httpOverlaySchema;
|
|
45
46
|
constructor(kernel) {
|
|
46
47
|
super("http", kernel, import.meta.url, config);
|
|
47
48
|
this.addCommand(Network);
|
|
@@ -68,7 +69,8 @@ let Http = class Http extends Module {
|
|
|
68
69
|
* certificats `serialNumber`) — cf `defineHttpConfig`.
|
|
69
70
|
*/
|
|
70
71
|
async onKernelRegister() {
|
|
71
|
-
|
|
72
|
+
const input = this.options;
|
|
73
|
+
this.options = defineHttpConfig(input ?? {}, this.kernel);
|
|
72
74
|
return this;
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -80,7 +80,7 @@ var AssetsPublish = class extends Command {
|
|
|
80
80
|
}
|
|
81
81
|
/** Sources = mounts natifs (`server-static`) + bundles `@nodefony/frontend`. */
|
|
82
82
|
collectSources() {
|
|
83
|
-
const modules = this.kernel?.getModules
|
|
83
|
+
const modules = this.kernel?.getModules() ?? {};
|
|
84
84
|
const sources = [];
|
|
85
85
|
const staticSvc = modules["http"]?.get("server-static");
|
|
86
86
|
staticSvc?.mountModulePublics?.();
|
|
@@ -21,7 +21,7 @@ var Certificates = class extends Command {
|
|
|
21
21
|
this.addOption("-j, --json", "output the certificate summary as JSON");
|
|
22
22
|
}
|
|
23
23
|
async generate(opts) {
|
|
24
|
-
const service = (this.kernel?.getModules()
|
|
24
|
+
const service = (this.kernel?.getModules()["http"])?.get("certificates") ?? null;
|
|
25
25
|
if (!service) {
|
|
26
26
|
this.log("Service 'certificates' introuvable (module @nodefony/http non chargé).", "ERROR");
|
|
27
27
|
return this;
|
|
@@ -16,7 +16,7 @@ var Network = class extends Command {
|
|
|
16
16
|
let network = this.kernel?.getNetwork();
|
|
17
17
|
let result = {};
|
|
18
18
|
if (arg) {
|
|
19
|
-
if (network?.interfaces[arg]) result = network
|
|
19
|
+
if (network?.interfaces[arg]) result = network.interfaces[arg];
|
|
20
20
|
} else result = network?.interfaces;
|
|
21
21
|
if (opts.json) process.stdout.write(`${JSON.stringify(result, void 0, " ")}\n`);
|
|
22
22
|
else process.stdout.write(`${inspect(result, { colors: process.stdout.isTTY })}\n`);
|
|
@@ -43,7 +43,7 @@ var ProxyGenerate = class extends Command {
|
|
|
43
43
|
}
|
|
44
44
|
/** Construit le modèle d'introspection depuis le kernel + le service statique. */
|
|
45
45
|
buildIntrospection(opts) {
|
|
46
|
-
const module = this.kernel?.getModules()
|
|
46
|
+
const module = this.kernel?.getModules()["http"];
|
|
47
47
|
const httpOpts = module?.options ?? {};
|
|
48
48
|
const servers = (this.kernel?.options)?.servers;
|
|
49
49
|
const staticSvc = module?.get("server-static");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { overlayField } from "nodefony";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
//#region nodefony/config/config.ts
|
|
3
4
|
/**
|
|
@@ -327,9 +328,29 @@ const httpConfigSchema = z.strictObject({
|
|
|
327
328
|
})
|
|
328
329
|
}).describe("Configuration de @nodefony/http.");
|
|
329
330
|
/**
|
|
331
|
+
* LISTE BLANCHE des clés qu'une requête peut surcharger pour elle seule
|
|
332
|
+
* (`overlayConfig("@nodefony/http", …)`, #494) : les quotas de corps et
|
|
333
|
+
* d'envoi, lus au MOMENT de lire le corps (`Request.readBodyLimits`) — un
|
|
334
|
+
* calque posé avant la lecture du corps (écouteur `onRequestScope`, organisation
|
|
335
|
+
* reconnue à son nom d'hôte) les voit. Tout le reste est refusé : en-têtes de
|
|
336
|
+
* sécurité, pare-feu, `trustProxy`, sessions, limites de débit, certificats —
|
|
337
|
+
* et `queryString`, lu au constructeur de la requête, avant tout calque.
|
|
338
|
+
* Chaque champ réutilise celui du schéma (mêmes bornes) sans son défaut.
|
|
339
|
+
*/
|
|
340
|
+
const httpOverlaySchema = z.strictObject({
|
|
341
|
+
maxBodySize: overlayField(httpConfigSchema.shape.maxBodySize),
|
|
342
|
+
upload: z.strictObject({
|
|
343
|
+
maxFileSize: overlayField(uploadSchema.shape.maxFileSize),
|
|
344
|
+
maxTotalFileSize: overlayField(uploadSchema.shape.maxTotalFileSize),
|
|
345
|
+
maxFiles: overlayField(uploadSchema.shape.maxFiles),
|
|
346
|
+
maxFields: overlayField(uploadSchema.shape.maxFields),
|
|
347
|
+
maxFieldsSize: overlayField(uploadSchema.shape.maxFieldsSize)
|
|
348
|
+
}).partial()
|
|
349
|
+
}).partial().describe("Clés de @nodefony/http surchargeables par requête.");
|
|
350
|
+
/**
|
|
330
351
|
* Défauts du module, matérialisés depuis le schéma (source unique). Toujours
|
|
331
352
|
* valides par construction ; passés au `super(..., config)` du Module class.
|
|
332
353
|
*/
|
|
333
354
|
const config = httpConfigSchema.parse({});
|
|
334
355
|
//#endregion
|
|
335
|
-
export { config as default, httpConfigSchema };
|
|
356
|
+
export { config as default, httpConfigSchema, httpOverlaySchema };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -51,7 +51,8 @@ function createProfilerAdminApi(profiler) {
|
|
|
51
51
|
path: "{id}",
|
|
52
52
|
summary: "Full profile (phase timeline) for a requestId",
|
|
53
53
|
handler: ({ params }) => {
|
|
54
|
-
const
|
|
54
|
+
const { id = "" } = params;
|
|
55
|
+
const entry = profiler.get(id);
|
|
55
56
|
if (!entry) return {
|
|
56
57
|
status: 404,
|
|
57
58
|
body: {
|
|
@@ -2,6 +2,19 @@ import { RequestContext } from "nodefony";
|
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
3
|
//#region nodefony/service/audit-logger.ts
|
|
4
4
|
/**
|
|
5
|
+
* Durée totale de la requête, de la première phase (`startMs`) à maintenant.
|
|
6
|
+
* `null` si le chronométrage est coupé (aucune phase enregistrée). Partagée
|
|
7
|
+
* par le journal d'audit et le journal lisible — une seule définition.
|
|
8
|
+
*
|
|
9
|
+
* @param phases - phases chronométrées du contexte
|
|
10
|
+
* @returns durée en millisecondes, ou `null`
|
|
11
|
+
*/
|
|
12
|
+
function computeDurationMs(phases) {
|
|
13
|
+
const first = phases.at(0);
|
|
14
|
+
if (first === void 0 || typeof first.startMs !== "number") return null;
|
|
15
|
+
return performance.now() - first.startMs;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
5
18
|
* Severity derived from HTTP status code — RFC 9110 categories.
|
|
6
19
|
* 1xx/2xx/3xx → INFO ; 4xx → WARNING ; 5xx → ERROR.
|
|
7
20
|
* Unknown/missing status → INFO.
|
|
@@ -77,7 +90,7 @@ var JsonAuditLogger = class {
|
|
|
77
90
|
method: ctx.method,
|
|
78
91
|
url: ctx.url,
|
|
79
92
|
status,
|
|
80
|
-
durationMs:
|
|
93
|
+
durationMs: computeDurationMs(ctx.phases),
|
|
81
94
|
remoteAddress: ctx.remoteAddress ?? null,
|
|
82
95
|
host: ctx.getHost?.() ?? null,
|
|
83
96
|
userAgent: ctx.getUserAgent?.() ?? null,
|
|
@@ -108,7 +121,7 @@ var JsonAuditLogger = class {
|
|
|
108
121
|
method: ctx.method,
|
|
109
122
|
url: ctx.url,
|
|
110
123
|
status,
|
|
111
|
-
durationMs:
|
|
124
|
+
durationMs: computeDurationMs(ctx.phases),
|
|
112
125
|
remoteAddress: ctx.remoteAddress ?? null,
|
|
113
126
|
host: ctx.getHost?.() ?? null,
|
|
114
127
|
userAgent: ctx.getUserAgent?.() ?? null,
|
|
@@ -128,16 +141,6 @@ var JsonAuditLogger = class {
|
|
|
128
141
|
};
|
|
129
142
|
}
|
|
130
143
|
/**
|
|
131
|
-
* Total request duration computed from the first phase startMs to now.
|
|
132
|
-
* Returns null if timing is disabled (no phases recorded).
|
|
133
|
-
*/
|
|
134
|
-
computeDurationMs(phases) {
|
|
135
|
-
if (!phases.length) return null;
|
|
136
|
-
const first = phases[0];
|
|
137
|
-
if (typeof first.startMs !== "number") return null;
|
|
138
|
-
return performance.now() - first.startMs;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
144
|
* Serialise an Error (recursively for `cause` chain) into an AuditErrorEntry.
|
|
142
145
|
* Stack is included only when `includeStack === true` (dev default).
|
|
143
146
|
* Cause chain is capped at `maxCauseDepth`.
|
|
@@ -156,4 +159,4 @@ var JsonAuditLogger = class {
|
|
|
156
159
|
}
|
|
157
160
|
};
|
|
158
161
|
//#endregion
|
|
159
|
-
export { JsonAuditLogger as default, severityFromStatus };
|
|
162
|
+
export { computeDurationMs, JsonAuditLogger as default, severityFromStatus };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asServersConfig } from "../src/servers/kernelServers.js";
|
|
1
2
|
import { Service, extend } from "nodefony";
|
|
2
3
|
import { randomBytes } from "node:crypto";
|
|
3
4
|
import fsp from "node:fs/promises";
|
|
@@ -31,6 +32,15 @@ const defaultOptions = {
|
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
35
|
+
* Valeur texte d'un champ de certificat node-forge (`getField` rend `any`),
|
|
36
|
+
* ou `null` si le champ est absent.
|
|
37
|
+
*/
|
|
38
|
+
function certFieldValue(field) {
|
|
39
|
+
if (field === null || typeof field !== "object") return null;
|
|
40
|
+
const value = field.value;
|
|
41
|
+
return typeof value === "string" ? value : String(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
34
44
|
* Service de fourniture du certificat TLS du serveur HTTPS/WSS.
|
|
35
45
|
*
|
|
36
46
|
* Trois stratégies : `explicit` (certificat fourni en config — le cas de
|
|
@@ -90,7 +100,7 @@ var Certificate = class Certificate extends Service {
|
|
|
90
100
|
* ne l'appelle jamais.
|
|
91
101
|
*/
|
|
92
102
|
async loadForge() {
|
|
93
|
-
|
|
103
|
+
this.forge ??= (await import("node-forge")).default;
|
|
94
104
|
return this.forge;
|
|
95
105
|
}
|
|
96
106
|
/** Accès au backend forge déjà chargé (lève si `loadForge` n'a pas été appelé). */
|
|
@@ -120,7 +130,7 @@ var Certificate = class Certificate extends Service {
|
|
|
120
130
|
* exemplaire. La promesse était écrite ; rien ne la tenait.
|
|
121
131
|
*/
|
|
122
132
|
get tlsWanted() {
|
|
123
|
-
return !!this.module.kernel?.options
|
|
133
|
+
return !!asServersConfig(this.module.kernel?.options.servers)?.https;
|
|
124
134
|
}
|
|
125
135
|
async init() {
|
|
126
136
|
if (!this.tlsWanted) return this;
|
|
@@ -138,8 +148,8 @@ var Certificate = class Certificate extends Service {
|
|
|
138
148
|
*/
|
|
139
149
|
static generateSerialHex() {
|
|
140
150
|
const bytes = randomBytes(16);
|
|
141
|
-
bytes
|
|
142
|
-
|
|
151
|
+
const head = bytes.readUInt8(0) & 127;
|
|
152
|
+
bytes[0] = head === 0 ? 1 : head;
|
|
143
153
|
return bytes.toString("hex");
|
|
144
154
|
}
|
|
145
155
|
setFiles() {
|
|
@@ -179,7 +189,7 @@ var Certificate = class Certificate extends Service {
|
|
|
179
189
|
path.dirname(this.certPath),
|
|
180
190
|
path.dirname(this.fullchainPath)
|
|
181
191
|
];
|
|
182
|
-
for
|
|
192
|
+
for (const directory of directories) try {
|
|
183
193
|
await fsp.access(directory);
|
|
184
194
|
} catch {
|
|
185
195
|
await fsp.mkdir(directory, { recursive: true });
|
|
@@ -237,7 +247,7 @@ var Certificate = class Certificate extends Service {
|
|
|
237
247
|
}
|
|
238
248
|
if (requested === "auto" && this.hasExplicitCert()) return "explicit";
|
|
239
249
|
if (requested === "mkcert" || requested === "auto") {
|
|
240
|
-
if (this.isDev() && this.certOptions.dev
|
|
250
|
+
if (this.isDev() && this.certOptions.dev.useMkcert !== false) {
|
|
241
251
|
const caRoot = await this.detectMkcert();
|
|
242
252
|
if (caRoot) {
|
|
243
253
|
this.mkcertCaRoot = caRoot;
|
|
@@ -345,8 +355,8 @@ var Certificate = class Certificate extends Service {
|
|
|
345
355
|
const ext = cert.getExtension("subjectAltName");
|
|
346
356
|
if (!ext || !this.sanCovers(ext.altNames ?? [])) return false;
|
|
347
357
|
if (strategy === "mkcert") {
|
|
348
|
-
const org = cert.issuer.getField("O");
|
|
349
|
-
return
|
|
358
|
+
const org = certFieldValue(cert.issuer.getField("O"));
|
|
359
|
+
return org !== null && /mkcert/i.test(org);
|
|
350
360
|
}
|
|
351
361
|
return cert.signatureOid !== pki.oids.sha1WithRSAEncryption;
|
|
352
362
|
} catch {
|
|
@@ -401,7 +411,7 @@ var Certificate = class Certificate extends Service {
|
|
|
401
411
|
}
|
|
402
412
|
async writeCertificates(force = false) {
|
|
403
413
|
await this.ensureDirectoriesExist();
|
|
404
|
-
for
|
|
414
|
+
for (const file of this.files) try {
|
|
405
415
|
if (file.variable) {
|
|
406
416
|
const mode = file.path === this.privateKeyPath ? this.certOptions.privateKeyMode ?? 384 : 420;
|
|
407
417
|
if (force) await fsp.rm(file.path, { force: true });
|
|
@@ -542,8 +552,8 @@ var Certificate = class Certificate extends Service {
|
|
|
542
552
|
info.validFrom = cert.validity.notBefore.toISOString();
|
|
543
553
|
info.validTo = cert.validity.notAfter.toISOString();
|
|
544
554
|
info.signatureAlgorithm = this.oidName(cert.signatureOid, pki);
|
|
545
|
-
const cn = cert.subject.getField("CN");
|
|
546
|
-
if (cn) info.commonName =
|
|
555
|
+
const cn = certFieldValue(cert.subject.getField("CN"));
|
|
556
|
+
if (cn !== null) info.commonName = cn;
|
|
547
557
|
const ext = cert.getExtension("subjectAltName");
|
|
548
558
|
if (ext?.altNames) info.san = ext.altNames.map((a) => a.type === 7 ? a.ip ?? "" : a.value ?? "");
|
|
549
559
|
} catch {}
|
|
@@ -560,9 +570,11 @@ var Certificate = class Certificate extends Service {
|
|
|
560
570
|
case "sha512": return md.sha512.create();
|
|
561
571
|
case "sha384": return md.sha384.create();
|
|
562
572
|
case "sha256": return md.sha256.create();
|
|
563
|
-
default:
|
|
564
|
-
|
|
573
|
+
default: {
|
|
574
|
+
const refused = hash;
|
|
575
|
+
this.log(`Hachage '${refused}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
|
|
565
576
|
return md.sha256.create();
|
|
577
|
+
}
|
|
566
578
|
}
|
|
567
579
|
}
|
|
568
580
|
};
|
|
@@ -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
|
}
|
|
@@ -145,6 +145,106 @@ function isUniqueViolation(error) {
|
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
+
* Reconnaît une suppression refusée parce que l'entité est encore RÉFÉRENCÉE
|
|
149
|
+
* (`ReferencedEntityError` de `@nodefony/orm-core`) et rend le nom de l'entité
|
|
150
|
+
* qui la désigne.
|
|
151
|
+
*
|
|
152
|
+
* Reconnaissance **structurelle** (`name` + `referencedBy`), sans importer
|
|
153
|
+
* orm-core : `@nodefony/http` n'en dépend pas, et l'erreur reste une erreur de
|
|
154
|
+
* DONNÉES que chaque surface projette elle-même — ici en 409, un conflit d'état
|
|
155
|
+
* (RFC 9110 §15.5.10) : la requête est valide, c'est l'existence d'un enfant qui
|
|
156
|
+
* la refuse.
|
|
157
|
+
*
|
|
158
|
+
* @param error - erreur remontée par le pipeline.
|
|
159
|
+
* @returns le nom de l'entité référençante, ou `null`.
|
|
160
|
+
*/
|
|
161
|
+
function referencedByOf(error) {
|
|
162
|
+
let current = error;
|
|
163
|
+
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
164
|
+
const candidate = current;
|
|
165
|
+
if (candidate.name === "ReferencedEntityError" && typeof candidate.referencedBy === "string") return candidate.referencedBy;
|
|
166
|
+
current = candidate.cause;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Types de colonne dont une valeur mal formée est un IDENTIFIANT mal formé,
|
|
172
|
+
* tel que PostgreSQL le nomme dans son message `22P02`.
|
|
173
|
+
*
|
|
174
|
+
* Restreint aux types qui portent des clés : `22P02` couvre aussi une date ou
|
|
175
|
+
* un JSON illisibles, que le contrat d'entrée rejette avant la base — les
|
|
176
|
+
* retenir ici déguiserait en faute du client une valeur fabriquée par le code.
|
|
177
|
+
*/
|
|
178
|
+
const IDENTIFIER_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
179
|
+
"uuid",
|
|
180
|
+
"integer",
|
|
181
|
+
"bigint",
|
|
182
|
+
"smallint"
|
|
183
|
+
]);
|
|
184
|
+
/**
|
|
185
|
+
* Signature figée du message `22P02` de PostgreSQL : le type visé, puis la
|
|
186
|
+
* valeur refusée entre guillemets. Même statut que {@link SQLITE_SCHEMA_PREFIXES} :
|
|
187
|
+
* un début de message stable dans le source du moteur, lu seulement APRÈS le code.
|
|
188
|
+
*/
|
|
189
|
+
const PG_INVALID_INPUT = /^invalid input syntax for type (\w+): "(.*)"$/su;
|
|
190
|
+
/**
|
|
191
|
+
* Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
|
|
192
|
+
*
|
|
193
|
+
* Deux moteurs le disent : MongoDB par une `CastError` de Mongoose (la clé est
|
|
194
|
+
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier).
|
|
195
|
+
* SQLite et MySQL rangent la clé en texte : ils ne lèvent rien, la lecture rend
|
|
196
|
+
* simplement « rien », d'où leur 404 — c'est la cohérence avec eux qui est visée.
|
|
197
|
+
*
|
|
198
|
+
* Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
|
|
199
|
+
*
|
|
200
|
+
* @param error - erreur remontée par le pipeline.
|
|
201
|
+
* @returns la valeur refusée, ou `null` si ce n'est pas ce cas.
|
|
202
|
+
*/
|
|
203
|
+
function malformedIdentifierOf(error) {
|
|
204
|
+
let current = error;
|
|
205
|
+
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
206
|
+
const candidate = current;
|
|
207
|
+
if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
|
|
208
|
+
if (String(candidate.code) === "22P02") {
|
|
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
|
+
}
|
|
214
|
+
}
|
|
215
|
+
current = candidate.cause;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Statut d'un identifiant mal formé : **404** s'il vient du CHEMIN, **400** sinon.
|
|
221
|
+
*
|
|
222
|
+
* Le chemin DÉSIGNE une ressource : `/api/posts/abc` demande une ressource qui ne
|
|
223
|
+
* peut pas exister, et SQLite comme MySQL rendent déjà 404 — le client reçoit le
|
|
224
|
+
* même statut quel que soit le moteur. Une valeur venue d'ailleurs (filtre,
|
|
225
|
+
* corps) est un PARAMÈTRE invalide : un 404 y dirait « la collection n'existe
|
|
226
|
+
* pas », ce qui est faux. On compare aux segments DÉCODÉS du chemin — l'URL est
|
|
227
|
+
* reçue encodée, la valeur est ce que la base a vu.
|
|
228
|
+
*
|
|
229
|
+
* @param value - la valeur refusée par la base.
|
|
230
|
+
* @param url - l'URL de la requête (`context.url`).
|
|
231
|
+
*/
|
|
232
|
+
function malformedIdentifierStatus(value, url) {
|
|
233
|
+
if (!url) return 400;
|
|
234
|
+
let pathname;
|
|
235
|
+
try {
|
|
236
|
+
pathname = new URL(url, "http://nodefony.invalid").pathname;
|
|
237
|
+
} catch {
|
|
238
|
+
return 400;
|
|
239
|
+
}
|
|
240
|
+
for (const segment of pathname.split("/")) try {
|
|
241
|
+
if (decodeURIComponent(segment) === value) return 404;
|
|
242
|
+
} catch {
|
|
243
|
+
if (segment === value) return 404;
|
|
244
|
+
}
|
|
245
|
+
return 400;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
148
248
|
* Codes rendus par les pilotes quand la requête porte sur une TABLE ou une
|
|
149
249
|
* COLONNE que la base n'a pas — la signature d'un schéma en retard sur le code.
|
|
150
250
|
*
|
|
@@ -210,7 +310,7 @@ function schemaMismatchOf(error) {
|
|
|
210
310
|
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
211
311
|
const candidate = current;
|
|
212
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";
|
|
213
|
-
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";
|
|
214
314
|
current = candidate.cause;
|
|
215
315
|
}
|
|
216
316
|
return null;
|
|
@@ -276,7 +376,7 @@ var DefaultErrorRenderer = class {
|
|
|
276
376
|
const httpError = this.toHttpError(error, context);
|
|
277
377
|
const rawCode = httpError.code;
|
|
278
378
|
let code = typeof rawCode === "number" && Number.isInteger(rawCode) ? rawCode : 500;
|
|
279
|
-
if (context
|
|
379
|
+
if (context.rejected === false) code = toWsCloseCode(code);
|
|
280
380
|
else if (code > 599) code = 500;
|
|
281
381
|
const internal = code === 1011 || code >= 500 && code <= 599;
|
|
282
382
|
const reason = isProduction() && internal ? OPAQUE_SERVER_ERROR : httpError.message;
|
|
@@ -295,9 +395,22 @@ var DefaultErrorRenderer = class {
|
|
|
295
395
|
}
|
|
296
396
|
if (isUniqueViolation(error)) {
|
|
297
397
|
const conflict = new nodefonyError(UNIQUE_VIOLATION_MESSAGE, CONFLICT_STATUS);
|
|
298
|
-
conflict.stack = error.stack;
|
|
398
|
+
if (error.stack !== void 0) conflict.stack = error.stack;
|
|
299
399
|
return new HttpError(conflict, CONFLICT_STATUS, context);
|
|
300
400
|
}
|
|
401
|
+
const referencedBy = referencedByOf(error);
|
|
402
|
+
if (referencedBy !== null) {
|
|
403
|
+
const conflict = new nodefonyError(`Conflict — this resource is still referenced by ${referencedBy}`, CONFLICT_STATUS);
|
|
404
|
+
if (error.stack !== void 0) conflict.stack = error.stack;
|
|
405
|
+
return new HttpError(conflict, CONFLICT_STATUS, context);
|
|
406
|
+
}
|
|
407
|
+
const malformed = malformedIdentifierOf(error);
|
|
408
|
+
if (malformed !== null) {
|
|
409
|
+
const status = malformedIdentifierStatus(malformed, context?.url);
|
|
410
|
+
const refused = new nodefonyError(status === 404 ? "Not Found — this identifier cannot designate any resource" : "Bad Request — malformed identifier", status);
|
|
411
|
+
if (error.stack !== void 0) refused.stack = error.stack;
|
|
412
|
+
return new HttpError(refused, status, context);
|
|
413
|
+
}
|
|
301
414
|
const code = error.code;
|
|
302
415
|
return new HttpError(error, typeof code === "number" ? code : void 0, context);
|
|
303
416
|
}
|
|
@@ -317,4 +430,4 @@ var DefaultErrorRenderer = class {
|
|
|
317
430
|
}
|
|
318
431
|
};
|
|
319
432
|
//#endregion
|
|
320
|
-
export { DefaultErrorRenderer as default, schemaMismatchOf };
|
|
433
|
+
export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf };
|