@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.
Files changed (98) hide show
  1. package/README.md +7 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +6 -4
  6. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  7. package/dist/nodefony/command/certificatesCommand.js +1 -1
  8. package/dist/nodefony/command/networkCommand.js +1 -1
  9. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  10. package/dist/nodefony/config/config.js +22 -1
  11. package/dist/nodefony/interfaces/IRouting.js +1 -0
  12. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  13. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  14. package/dist/nodefony/service/audit-logger.js +16 -13
  15. package/dist/nodefony/service/certificates.js +25 -13
  16. package/dist/nodefony/service/error-renderer.js +118 -5
  17. package/dist/nodefony/service/http-kernel.js +342 -165
  18. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  19. package/dist/nodefony/service/servers/clientError.js +2 -2
  20. package/dist/nodefony/service/servers/server-http.js +23 -23
  21. package/dist/nodefony/service/servers/server-https.js +41 -42
  22. package/dist/nodefony/service/servers/server-static.js +13 -17
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
  24. package/dist/nodefony/service/servers/server-websocket.js +11 -10
  25. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  26. package/dist/nodefony/service/sessions/sessions-service.js +25 -17
  27. package/dist/nodefony/service/trace.js +1 -1
  28. package/dist/nodefony/service/upload/upload-service.js +11 -10
  29. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  30. package/dist/nodefony/src/context/Context.js +34 -19
  31. package/dist/nodefony/src/context/forwarded.js +4 -4
  32. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  33. package/dist/nodefony/src/context/http/Request.js +93 -44
  34. package/dist/nodefony/src/context/http/Response.js +107 -51
  35. package/dist/nodefony/src/context/http/parser.js +9 -10
  36. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  37. package/dist/nodefony/src/context/http2/Response.js +53 -25
  38. package/dist/nodefony/src/context/metaData.js +1 -1
  39. package/dist/nodefony/src/context/trustProxy.js +4 -3
  40. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  42. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  43. package/dist/nodefony/src/cookies/cookie.js +3 -3
  44. package/dist/nodefony/src/errors/httpError.js +6 -6
  45. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  46. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  47. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  48. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  49. package/dist/nodefony/src/servers/portBinder.js +8 -21
  50. package/dist/nodefony/src/session/session.js +18 -10
  51. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  52. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  53. package/dist/types/index.d.ts +16 -0
  54. package/dist/types/nodefony/config/config.d.ts +22 -0
  55. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  56. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  57. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  58. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  61. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  62. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  63. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  64. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  65. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  66. package/dist/types/nodefony/service/error-renderer.d.ts +14 -0
  67. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  68. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  69. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  70. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  71. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  72. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  73. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  74. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  75. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  76. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  77. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  78. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  79. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  80. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  81. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  83. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  84. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  85. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  86. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  87. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  88. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  89. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  90. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  91. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  92. package/docs/cookies.md +25 -25
  93. package/docs/observabilite.md +30 -30
  94. package/docs/rate-limit.md +22 -22
  95. package/docs/servers.md +61 -61
  96. package/docs/session.md +82 -77
  97. package/docs/upload.md +43 -37
  98. 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.150.0/helpers/esm/decorate.js
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.150.0/helpers/esm/decorateMetadata.js
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
  }
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
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.150.0/helpers/esm/decorateMetadata.js";
25
- import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
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
- this.options = defineHttpConfig(this.options ?? {}, this.kernel);
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()?.["http"])?.get("certificates") ?? null;
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?.interfaces[arg];
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()?.["http"];
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 entry = profiler.get(params.id);
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: this.computeDurationMs(ctx.phases),
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: this.computeDurationMs(ctx.phases),
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
- if (!this.forge) this.forge = (await import("node-forge")).default;
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?.servers?.https;
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[0] &= 127;
142
- if (bytes[0] === 0) bytes[0] = 1;
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 await (const directory of directories) try {
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?.useMkcert !== false) {
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 Boolean(org && /mkcert/i.test(String(org.value)));
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 await (const file of this.files) try {
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 = String(cn.value);
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
- this.log(`Hachage '${hash}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
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?.startsWith(p))) return "probable";
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 && context.rejected === false) code = toWsCloseCode(code);
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 };