@nodefony/http 10.0.0-alpha.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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,335 @@
1
+ import { z } from "zod";
2
+ //#region nodefony/config/config.ts
3
+ /**
4
+ * @nodefony/http — CONFIGURATION DU MODULE (schéma Zod = source unique).
5
+ *
6
+ * ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` = la valeur d'usine ;
7
+ * changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
8
+ * surcharge via `use("@nodefony/...", { … })` dans SON `nodefony.config.ts`.
9
+ *
10
+ * RÈGLE D'OR (ADR-0006) : ce fichier porte le **schéma Zod commenté** (type +
11
+ * validation + défaut + doc) ET matérialise les défauts via `parse({})`. Aucune
12
+ * valeur n'est re-tapée ailleurs. Le builder (`defineModuleConfig.ts` →
13
+ * `defineHttpConfig`) et les types (`interfaces/IHttpConfig.ts`) importent le
14
+ * schéma D'ICI (nœud bas : ce fichier n'importe que `zod` → pas de cycle).
15
+ *
16
+ * La config est validée au boot du Module class (hook `onKernelRegister`, via le
17
+ * builder {@link import("./defineModuleConfig").defineHttpConfig}) → plante
18
+ * propre avec messages clairs si la config est invalide, plutôt qu'un
19
+ * `undefined.x` silencieux en runtime.
20
+ *
21
+ * ## `strict` (refus) vs `loose` (passthrough) — décision de design
22
+ *
23
+ * Deux formes, et **jamais `z.object`** — dont le comportement par défaut
24
+ * (RETIRER en silence les clés inconnues) est le seul des trois qu'on ne veuille
25
+ * nulle part. `use("@nodefony/http", { trustProxi: true })` était accepté, la
26
+ * clé disparaissait, l'application démarrait avec le DÉFAUT et personne n'était
27
+ * prévenu : celui qui croyait armer la confiance de proxy ne l'avait pas armée.
28
+ *
29
+ * - **`z.strictObject`** pour les sections **100 % consommées par notre code**
30
+ * (`securityHeaders`, `certificates`, `session`, `upload`, `rateLimit`, et la
31
+ * RACINE — c'est elle qui porte `trustProxy`) : une clé inconnue INTERROMPT le
32
+ * boot en la nommant. Le typage (registre `NodefonyModuleConfig`) attrape la
33
+ * faute de frappe à la COMPILATION ; ceci l'attrape au DÉMARRAGE, pour tout ce
34
+ * qui contourne le compilateur — une config lue d'un fichier, un `as never`,
35
+ * un override d'environnement (`NF__HTTP__…`).
36
+ * - **`z.looseObject`** pour les sections **transmises telles quelles à une lib
37
+ * tierce** (node:http/https/http2, `ws`, `qs`, `serve-static`) : y refuser une
38
+ * clé interdirait une option lib légitime non listée ici (ex.
39
+ * `http: { insecureHTTPParser: true }`). C'est leur raison d'être, pas un
40
+ * relâchement.
41
+ *
42
+ * Le refus passe par `parseModuleConfig` (cœur), qui lève une
43
+ * **`BootConfigurationError`** — et pas une `Error` ordinaire, que le kernel
44
+ * absorberait en développement (fail-soft), c'est-à-dire exactement là où la
45
+ * faute vient d'être écrite.
46
+ *
47
+ * ## Métadonnées de champ (`.meta()` natif zod)
48
+ *
49
+ * Certains champs portent des flags Nodefony (`reserved`, `runtimeMutable`,
50
+ * `kernelDerived`, `secret` — cf `IConfigFieldMeta` du core) via le `.meta()`
51
+ * NATIF de zod → recopiés par `z.toJSONSchema()` pour Studio / la doc / un
52
+ * futur reload-à-chaud. ⚠️ `.meta()` TOUJOURS EN DERNIER dans la chaîne
53
+ * (chaque méthode zod clone l'instance → un `.default()` APRÈS `.meta()`
54
+ * perdrait la métadonnée).
55
+ *
56
+ * ## Pureté
57
+ *
58
+ * Ce schéma reste PUR (pas de `Nodefony.getKernel()` ni `process.env`) → sa
59
+ * sortie est déterministe et sérialisable en JSON Schema (`httpConfigJsonSchema`
60
+ * dans `defineModuleConfig.ts`). Les défauts dérivés du kernel
61
+ * (`upload.uploadDir` ← `kernel.tmpDir`, `certificates.openssl.attrs` ←
62
+ * `kernel.domain`/`projectName`) sont injectés APRÈS le parse, dans le builder
63
+ * (kernel disponible à `onKernelRegister`).
64
+ *
65
+ * ⚠️ Piège Zod 4 : un `.default({})` plat NE ré-applique PAS les sous-défauts du
66
+ * sous-schéma. Pattern obligatoire : extraire chaque sous-schéma en const +
67
+ * `.default(() => sub.parse({}))` (le callback force la ré-évaluation).
68
+ *
69
+ * SURCHARGE PAR L'APPLICATION (fusion récursive) :
70
+ *
71
+ * // nodefony.config.ts
72
+ * use("@nodefony/http", {
73
+ * session: { name: "myapp", store: "drizzle" },
74
+ * statics: { assets: { path: "public/assets" } },
75
+ * })
76
+ *
77
+ * NB : le format de log par requête (`pretty`/`json`/`default`) se configure au
78
+ * niveau KERNEL (`log.requestFormat`), pas ici — voir
79
+ * `HttpKernel.applyRequestLoggerFromConfig()`.
80
+ */
81
+ const strictTransportSecuritySchema = z.strictObject({
82
+ maxAge: z.number().int().nonnegative().default(31536e3).meta({
83
+ runtimeMutable: true,
84
+ description: "Durée (s) pendant laquelle le navigateur force HTTPS. Défaut 1 an (recommandation OWASP). Posé uniquement sur réponses HTTPS/HTTP2. Éditable à chaud (recompute HttpKernel → en-tête HSTS recalculé)."
85
+ }),
86
+ includeSubDomains: z.boolean().default(true).meta({
87
+ runtimeMutable: true,
88
+ description: "Étend le HSTS à tous les sous-domaines. Défaut true (OWASP). Éditable à chaud (recompute HttpKernel)."
89
+ }),
90
+ preload: z.boolean().default(false).meta({
91
+ runtimeMutable: true,
92
+ description: "Inscrit le domaine à la HSTS preload list — ENGAGEMENT IRRÉVERSIBLE. Ne pas activer sans avoir lu https://hstspreload.org/#removal. Éditable à chaud (effet immédiat sur l'en-tête, mais l'inscription preload réelle est externe et irréversible)."
93
+ })
94
+ }).describe("HSTS (RFC 6797). Mettre la section à `null` pour ne pas émettre le header.");
95
+ const securityHeadersSchema = z.strictObject({
96
+ contentTypeOptions: z.string().nullable().default("nosniff").meta({
97
+ runtimeMutable: true,
98
+ description: "X-Content-Type-Options — bloque le MIME-sniffing. Valeur RFC `nosniff` (seule reconnue). `null` = header désactivé. Relu à chaque requête (éditable à chaud via le recompute HttpKernel)."
99
+ }),
100
+ frameOptions: z.string().nullable().default("DENY").meta({
101
+ runtimeMutable: true,
102
+ description: "X-Frame-Options — anti-clickjacking. `DENY` (recommandé) | `SAMEORIGIN`. Superposé par CSP `frame-ancestors`. `null` = désactivé. Éditable à chaud (recompute HttpKernel)."
103
+ }),
104
+ strictTransportSecurity: strictTransportSecuritySchema.nullable().default(() => strictTransportSecuritySchema.parse({})).describe("HSTS — voir sous-schéma. `null` = header désactivé.")
105
+ }).describe("En-têtes de sécurité HTTP — defaults OWASP secure-by-default, posés en amont du pipeline (couvre aussi les statics).");
106
+ const uploadSchema = z.strictObject({
107
+ uploadDir: z.string().default("").meta({
108
+ kernelDerived: true,
109
+ description: "Répertoire de dépôt des fichiers uploadés. Vide (défaut) = résolu sur `kernel.tmpDir` par le builder. Chemin relatif = relatif à la racine projet."
110
+ }),
111
+ maxFileSize: z.number().int().positive().default(524288e3).meta({
112
+ runtimeMutable: true,
113
+ description: "Taille max d'UN fichier (busboy `limits.fileSize`, octets). 500 MB. Dépassement → 413. Réduire en production. Relu à chaque requête (éditable à chaud — tester un 413 d'upload sans redémarrer)."
114
+ }),
115
+ maxTotalFileSize: z.number().int().positive().default(524288e3).meta({
116
+ runtimeMutable: true,
117
+ description: "Taille CUMULÉE max de tous les fichiers d'une requête (octets, compteur Nodefony — busboy n'a pas de cumul natif). Anti-saturation disque. → 413. Relu à chaque requête (éditable à chaud)."
118
+ }),
119
+ maxFiles: z.number().int().nonnegative().default(1e3).meta({
120
+ runtimeMutable: true,
121
+ description: "Nombre max de fichiers par requête (busboy `limits.files`) — anti-DoS. Relu à chaque requête (éditable à chaud)."
122
+ }),
123
+ maxFields: z.number().int().nonnegative().default(1e3).meta({
124
+ runtimeMutable: true,
125
+ description: "Nombre max de champs texte (busboy `limits.fields`) — anti-abus. Relu à chaque requête (éditable à chaud)."
126
+ }),
127
+ maxFieldsSize: z.number().int().positive().default(2097152).meta({
128
+ runtimeMutable: true,
129
+ description: "Taille max d'UN champ texte (busboy `limits.fieldSize`, octets). 2 MB. Relu à chaque requête (éditable à chaud)."
130
+ }),
131
+ hashAlgorithm: z.union([z.literal(false), z.enum([
132
+ "sha256",
133
+ "sha1",
134
+ "md5"
135
+ ])]).default(false).describe("Hash calculé pendant le stream du fichier (intégrité). `false` (défaut, 0 coût CPU) | `sha256` | `sha1` | `md5`."),
136
+ encoding: z.string().default("utf-8").describe("Encodage par défaut des champs texte (busboy `defCharset`).")
137
+ }).describe("Sous-système d'upload busboy (streaming pur — fichiers écrits au fil de l'eau, jamais bufferisés en RAM).");
138
+ const queryStringSchema = z.looseObject({
139
+ parameterLimit: z.number().int().positive().default(1e3).describe("Nombre max de paramètres acceptés (qs `parameterLimit`)."),
140
+ delimiter: z.string().default("&").describe("Séparateur de paramètres. `&` = standard HTTP."),
141
+ ignoreQueryPrefix: z.boolean().default(true).describe("Ignore le `?` initial avant parsing (qs `ignoreQueryPrefix`).")
142
+ }).describe("Parseur de query string `qs`. Loose : toute option `qs` supplémentaire (depth, arrayLimit, allowDots…) est transmise telle quelle.");
143
+ const httpServerSchema = z.looseObject({
144
+ maxHeadersCount: z.number().int().nonnegative().default(2e3).describe("Nombre max d'en-têtes par requête — anti header flooding."),
145
+ keepAliveTimeout: z.number().int().nonnegative().default(5e3).describe("Délai keep-alive (ms) entre deux requêtes sur la même socket TCP."),
146
+ timeout: z.number().int().nonnegative().default(3e4).describe("Timeout global de la socket (ms). 0 = désactivé. Aligné sur responseTimeout : node arme ce timer à la connexion, et le framework ne ré-arme que si la valeur diffère (`socket.timeout !== ms`) — l'alignement économise 1 setTimeout par requête sans changer le comportement effectif (l'arm framework 30 s dominait déjà)."),
147
+ requestTimeout: z.number().int().nonnegative().default(3e4).describe("Timeout de réception requête complète (ms) — anti slow-loris."),
148
+ responseTimeout: z.number().int().nonnegative().default(3e4).describe("Timeout d'envoi de la réponse complète (ms)."),
149
+ shutdownTimeout: z.number().int().nonnegative().default(5e3).describe("Drain graceful au shutdown (ms) : délai laissé aux requêtes in-flight avant destruction forcée des sockets (SIGTERM/docker stop). Doit rester < grace period de l'orchestrateur (30 s k8s, 10 s Docker)."),
150
+ headers: z.record(z.string(), z.string()).nullable().default(null).describe("En-têtes ajoutés à toutes les réponses. `null` = aucun.")
151
+ }).describe("Serveur HTTP (node:http, port 5151). Loose : toute option `net.Server`/`http.Server` supplémentaire (insecureHTTPParser, maxRequestsPerSocket…) est transmise.");
152
+ const httpsServerSchema = httpServerSchema.extend({ rejectUnauthorized: z.boolean().default(false).describe("Rejette les certificats TLS non valides (auto-signés inclus). false en dev (certs auto-signés), TOUJOURS true en production.") }).describe("Serveur HTTPS (node:https + HTTP/2, port 5152). Loose : options TLS (ciphers, minVersion…) transmises.");
153
+ const http2Schema = z.looseObject({
154
+ maxConcurrentStreams: z.number().int().positive().default(100).describe("Flux concurrents max par session HTTP/2 — défense CVE-2023-44487 (Rapid Reset). Envoyé dans les SETTINGS."),
155
+ maxSessionMemory: z.number().int().positive().default(10).describe("Mémoire max (Mo) par session HTTP/2 — borne l'amplification mémoire.")
156
+ }).describe("Serveur HTTP/2 (sur le port HTTPS, allowHTTP1). Loose : options `http2.SecureServerOptions` transmises.");
157
+ const http3Schema = z.looseObject({});
158
+ const opensslAttrSchema = z.looseObject({
159
+ name: z.string().optional(),
160
+ value: z.string().optional(),
161
+ shortName: z.string().optional()
162
+ }).describe("Champ de sujet/issuer node-forge (CertificateField).");
163
+ const opensslSchema = z.strictObject({
164
+ size: z.number().int().positive().default(2048).describe("Taille de la clé RSA (bits). 2048 minimum, 4096 recommandé en prod."),
165
+ hash: z.enum([
166
+ "sha256",
167
+ "sha384",
168
+ "sha512"
169
+ ]).default("sha256").describe("Algorithme de hachage de la signature. SHA-1 INTERDIT (collision SHAttered 2017 ; CA/Browser Forum depuis 2016)."),
170
+ validityDays: z.number().int().positive().default(365).describe("Durée de validité du certificat (jours). ≤ 398 recommandé (CA/B Forum)."),
171
+ backdateMinutes: z.number().int().min(0).default(5).describe("Recul de notBefore (minutes) — tolère le décalage d'horloge du client (évite « certificate not yet valid »)."),
172
+ attrs: z.array(opensslAttrSchema).default([]).meta({
173
+ kernelDerived: true,
174
+ description: "Attributs du certificat (commonName, organizationName…). Vide (défaut) = dérivés du kernel par le builder (commonName ← domain)."
175
+ })
176
+ }).describe("Options de génération du certificat auto-signé (node-forge).");
177
+ const sanSchema = z.strictObject({
178
+ dns: z.array(z.string()).default([]).meta({
179
+ kernelDerived: true,
180
+ description: "Noms DNS du Subject Alternative Name (RFC 5280 §4.2.1.6). Vide = dérivé du kernel (localhost + domain)."
181
+ }),
182
+ ip: z.array(z.string()).default([]).describe("Adresses IP du SAN. Vide = dérivé (127.0.0.1, ::1).")
183
+ }).describe("Subject Alternative Name explicite. Vide = dérivé (localhost + domaine kernel). Chrome ignore le commonName depuis RFC 2818 → le SAN fait foi.");
184
+ const certDevSchema = z.strictObject({ useMkcert: z.boolean().default(true).describe("Préférer mkcert (CA locale trustée) en dev → HTTPS sans erreur navigateur (HMR/WSS). false = forcer le fallback auto-signé. Ignoré hors dev.") }).describe("Options de génération du certificat en développement.");
185
+ const certificatesSchema = z.strictObject({
186
+ strategy: z.enum([
187
+ "auto",
188
+ "mkcert",
189
+ "selfsigned",
190
+ "explicit"
191
+ ]).default("auto").describe("Stratégie de fourniture du certificat. `auto` (défaut) = mkcert si dispo en dev, sinon auto-signé. `explicit` = `key`/`cert` fournis (PROD : Let's Encrypt, ingress…). `mkcert`/`selfsigned` = forcer. La génération est un confort de DÉVELOPPEMENT — en prod, fournir un vrai certificat (Nodefony n'est pas une autorité de certification)."),
192
+ ca: z.string().default("").describe("Chemin du CA bundle. Vide = auto-signé."),
193
+ key: z.string().default("").describe("Chemin de la clé privée TLS. Vide = générée."),
194
+ cert: z.string().default("").describe("Chemin du certificat TLS. Vide = généré."),
195
+ privateKeyMode: z.number().int().default(384).meta({ description: "Permissions POSIX de la clé privée écrite sur disque (0600 = lecture/écriture owner uniquement). Une clé TLS ne doit JAMAIS être world-readable." }),
196
+ san: sanSchema.default(() => sanSchema.parse({})),
197
+ dev: certDevSchema.default(() => certDevSchema.parse({})),
198
+ openssl: opensslSchema.default(() => opensslSchema.parse({}))
199
+ }).describe("Certificats TLS du serveur HTTPS et WSS.");
200
+ const websocketSchema = z.looseObject({
201
+ keepaliveInterval: z.number().int().positive().default(2e4).describe("Intervalle (ms) des pings keep-alive — détecte les connexions zombies."),
202
+ keepaliveGracePeriod: z.number().int().positive().default(1e4).describe("Délai de grâce (ms) après un ping sans réponse avant fermeture."),
203
+ closeTimeout: z.number().int().positive().default(5e3).describe("Timeout (ms) de fermeture propre avant destruction de la socket."),
204
+ maxPayload: z.number().int().positive().default(1048576).describe("Taille max (octets) d'un message WS entrant. Au-delà → close RFC 6455 1009 « Message Too Big ». Défaut 1 MiB (secure-by-default anti DoS mémoire)."),
205
+ allowedOrigins: z.union([
206
+ z.boolean(),
207
+ z.string(),
208
+ z.array(z.string())
209
+ ]).default(false).describe("Allowlist d'`Origin` acceptées au handshake WS (anti-CSWSH, OWASP WSTG-CLNT-10 — les navigateurs n'appliquent PAS CORS aux WebSockets). `false` (DÉFAUT) = same-origin : l'`Origin` doit correspondre au `Host` (+ loopback toléré en development pour le dev cross-port). `true` = désactive le contrôle (Origin filtré en amont). string/liste = Origins cross-origin additionnelles : hostname exact (`app.example.com`) ou wildcard un-label (`*.example.com`). Une requête SANS `Origin` (client non-navigateur) est toujours acceptée."),
210
+ perMessageDeflate: z.union([z.boolean(), z.looseObject({
211
+ serverNoContextTakeover: z.boolean().optional().describe("Le serveur ne conserve PAS le contexte de compression entre messages (moins de RAM par connexion, ratio de compression plus faible)."),
212
+ clientNoContextTakeover: z.boolean().optional().describe("Idem côté client (négocié dans le handshake)."),
213
+ serverMaxWindowBits: z.number().int().min(8).max(15).optional().describe("Taille de la fenêtre LZ77 côté serveur (8-15) ; plus bas = moins de RAM."),
214
+ clientMaxWindowBits: z.number().int().min(8).max(15).optional().describe("Taille de la fenêtre LZ77 côté client (8-15)."),
215
+ threshold: z.number().int().nonnegative().optional().describe("Taille min (octets) d'un message pour être compressé. Défaut `ws` 1024."),
216
+ concurrencyLimit: z.number().int().positive().optional().describe("Nombre max d'opérations zlib concurrentes (back-pressure CPU). Défaut `ws` 10.")
217
+ })]).default(false).describe("Compression permessage-deflate (RFC 7692). Défaut false : la compression coûte CPU + mémoire et expose au zip-bomb (un petit payload compressé se décompresse en message géant). `true` = options par défaut, ou objet pour régler fenêtres / seuil / concurrence. N'activer que si le gain réseau le justifie."),
218
+ skipUTF8Validation: z.boolean().default(false).describe("Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). Gain CPU au prix de l'acceptation de frames texte invalides — laisser false sauf charge extrême maîtrisée."),
219
+ autoPong: z.boolean().default(true).describe("Répond automatiquement par un pong à chaque ping entrant (RFC 6455 §5.5.2-5.5.3). Laisser true — requis par le protocole et par le heartbeat des pairs."),
220
+ allowSynchronousEvents: z.boolean().default(true).describe("Émet les events ('message', 'ping'…) de plusieurs frames lues dans un même chunk réseau de façon synchrone (ws@8.18+). true = meilleur débit ; false = un event par tick d'event loop (latence plus régulière, plus équitable entre connexions)."),
221
+ maxBackpressure: z.number().int().nonnegative().default(4194304).describe("Seuil (octets) du buffer d'envoi par connexion (`ws.bufferedAmount`) au-delà duquel `backpressurePolicy` s'applique. Borne la RAM d'envoi face à un client LENT À RECEVOIR (anti-OOM) ; broadcast() amplifie (un seul lent peut plomber la diffusion). 0 = désactivé. Défaut 4 MiB."),
222
+ backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("Nombre de frames refusées D'AFFILÉE au-delà duquel la connexion est FERMÉE (1013), en politique 'drop'. Une frame qui repart remet le compteur à zéro. POURQUOI une série et pas un second seuil d'octets : une fois qu'on jette, plus rien n'alimente la file — elle plafonne au seuil de drop et n'atteint jamais un seuil supérieur, qui serait donc inatteignable (mesuré). Une série de refus, elle, distingue le pic passager du client mort. 0 = ne jamais fermer. Défaut 1000."),
223
+ backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport.")
224
+ }).describe("Serveur WebSocket (`ws@8`). Les options ci-dessus (+ loose : `verifyClient`, `handleProtocols`, `path`, `WebSocket`…) sont transmises à `ws`. GÉRÉ par Nodefony et donc NON exposé : `server`/`host`/`port` (attaché au serveur HTTP), `noServer` (forcé off), `clientTracking` (forcé true — requis par broadcast() + heartbeat), `backlog` (celui du serveur HTTP). `keepalive*`/`closeTimeout` sont des knobs Nodefony, pas des options `ws`.");
225
+ const staticOptionsSchema = z.looseObject({
226
+ cacheControl: z.boolean().optional().describe("Active le header Cache-Control."),
227
+ maxAge: z.number().int().nonnegative().optional().describe("Durée de cache (selon l'entrée : secondes pour defaultOptions, ms côté serve-static).")
228
+ }).describe("Options serve-static d'une entrée statique. Loose : index, dotfiles, etag, lastModified… transmises telles quelles.");
229
+ const staticEntrySchema = z.looseObject({
230
+ path: z.string().describe("Chemin relatif à la racine projet."),
231
+ options: staticOptionsSchema.optional()
232
+ }).describe("Répertoire statique servi.");
233
+ const staticsSchema = z.looseObject({
234
+ enabled: z.boolean().default(true).describe("Active le serveur de fichiers statiques intégré. `false` = AUCUN montage config-driven (`web`/`assets`…) → 0 listener, 0 résolution de chemin au boot. À mettre en production quand un reverse-proxy/CDN (nginx, Caddy, Varnish) sert les statiques. N.B. : ne gate PAS les montages programmatiques addMount() (assets Vite via @nodefony/frontend)."),
235
+ defaultOptions: staticOptionsSchema.default(() => ({
236
+ cacheControl: true,
237
+ maxAge: 345600
238
+ })).describe("Options appliquées à TOUTES les entrées (sauf surcharge). maxAge en secondes."),
239
+ web: staticEntrySchema.default(() => ({
240
+ path: "public",
241
+ options: { maxAge: 2592e6 }
242
+ })).describe("Répertoire `web` servi par défaut depuis la racine projet.")
243
+ }).describe("Fichiers statiques (serve-static). Loose : chaque entrée additionnelle (`assets`, …) est transmise. Dev uniquement — préférer un reverse-proxy en prod.");
244
+ const sessionCookieSchema = z.strictObject({
245
+ maxAge: z.number().int().nonnegative().default(0).describe("Durée de vie (s). 0 = cookie de session (fermé avec le navigateur)."),
246
+ httpOnly: z.boolean().default(true).describe("Inaccessible via JS (document.cookie) — protection XSS."),
247
+ secure: z.boolean().default(true).describe("Envoyé uniquement via HTTPS. TOUJOURS true en production."),
248
+ signed: z.boolean().default(false).describe("Signe le cookie avec le secret HMAC du kernel."),
249
+ hostPrefix: z.union([z.boolean(), z.literal("auto")]).default("auto").describe("Préfixe `__Host-` du cookie de session (RFC 6265bis / OWASP : anti session-fixation cross-subdomain). `\"auto\"` (défaut) = appliqué sur transport TLS (https/wss) uniquement ; `true` = toujours (l'opérateur garantit le TLS côté client, ex. proxy terminant le TLS) ; `false` = jamais.")
250
+ }).describe("Options du cookie de session.");
251
+ const sessionSchema = z.strictObject({
252
+ strictMode: z.boolean().default(true).describe("Rejette les IDs de session inconnus (crée une nouvelle session)."),
253
+ name: z.string().min(1).default("nodefony").describe("Nom du cookie de session."),
254
+ store: z.string().min(1).default("auto").describe("Store de session : `auto` (défaut — suit l'infra : cache redis > database > sqlite local si drizzle chargé > repli memory) ou le nom d'un storage enregistré (`memory` | `drizzle` | `mongoose` | `redis`). `memory` = volatil (dev/charge). Vocabulaire unifié : données = `store` (≠ `driver`)."),
255
+ gcIntervalS: z.number().int().min(0).default(600).describe("Intervalle de purge des sessions expirées (s), exécutée HORS hot-path par un timer déterministe. 0 = timer désarmé (purge déléguée à un worker / k8s CronJob, ou TTL natif du store Redis). Remplace le tirage probabiliste PHP gc_probability/gc_divisor (famine à bas trafic + p99)."),
256
+ gcJitter: z.boolean().default(true).describe("Étale le départ du gc d'un délai aléatoire (≤ 60 s) par process — évite les balayages simultanés sur un store partagé en cluster (thundering herd)."),
257
+ idleTimeoutS: z.number().int().nonnegative().default(1800).describe("Idle timeout (NIST SP 800-63B-4 / OWASP, s) : inactivité MAX depuis la dernière activité (HTTP ou WS) avant invalidation. Rafraîchi par un « touch » throttlé — l'activité réelle prolonge la session sans réécrire le blob (NIST/OWASP : idle « since the last request »). Enforcement 100 % serveur. 0 = pas d'expiration par inactivité. Défaut 1800 = 30 min."),
258
+ absoluteTimeoutS: z.number().int().nonnegative().default(43200).describe("Absolute timeout (NIST SP 800-63B-4 / OWASP, s) : âge MAX d'une session depuis sa CRÉATION, JAMAIS prolongé par l'activité → borne la fenêtre d'exploitation d'un identifiant volé même sur une session maintenue active (re-auth forcée). Sans lui, le touch rendrait une session active quasi éternelle (OWASP §Idle Timeout). 0 = désactivé. Défaut 43200 = 12 h."),
259
+ refererCheck: z.boolean().default(false).describe("Vérifie que le Referer correspond au domaine courant."),
260
+ cookie: sessionCookieSchema.default(() => sessionCookieSchema.parse({}))
261
+ }).describe("Gestionnaire de sessions. Le cookie ne porte QU'UN identifiant opaque (32 octets CSPRNG, base64url) — rien n'est chiffré ni stocké côté client ; l'état vit dans le store. Stores : `auto` (défaut) · memory · drizzle · mongoose · redis (cf le champ `store`).");
262
+ const rateLimitSchema = z.strictObject({
263
+ enabled: z.boolean().default(false).meta({
264
+ runtimeMutable: true,
265
+ description: "Active le rate-limit général par IP (requêtes HTTP ET handshakes WebSocket, même compteur). Défaut false (opt-in : coût hot-path + souvent délégué à l'infra en cloud-native). Éditable à chaud (HttpKernel reconstruit le compteur)."
266
+ }),
267
+ windowS: z.number().int().positive().default(60).meta({
268
+ runtimeMutable: true,
269
+ description: "Largeur de la fenêtre fixe, en secondes. Le compteur par IP repart à zéro à chaque fenêtre. Défaut 60 s."
270
+ }),
271
+ max: z.number().int().positive().default(300).meta({
272
+ runtimeMutable: true,
273
+ description: "Nombre max de requêtes par IP et par fenêtre. Au-delà : `429` + `Retry-After`. Défaut 300 / 60 s (≈ 5 req/s soutenu). À ajuster selon l'application."
274
+ }),
275
+ maxTracked: z.number().int().min(1e3).default(1e5).meta({ description: "Borne mémoire : nombre max d'IP suivies simultanément. Au cap, purge des fenêtres expirées puis éviction FIFO. Défaut 100 000." }),
276
+ gcIntervalS: z.number().int().positive().default(300).meta({ description: "Intervalle (s) du balayage de purge des fenêtres expirées, hors hot-path (GcScheduler du core). Défaut 300 s." }),
277
+ gcJitter: z.boolean().default(true).meta({ description: "Étale le tick GC d'un jitter aléatoire (anti-thundering-herd multi-pod). Défaut true." })
278
+ }).describe("Rate-limit général par IP des requêtes HTTP (P0.3) — distinct du backoff de login de @nodefony/security. En-têtes `X-RateLimit-*` + `429` (RFC 6585).");
279
+ const healthSchema = z.looseObject({
280
+ enabled: z.boolean().default(true).meta({
281
+ runtimeMutable: true,
282
+ description: "Expose les probes de santé. Réponses minimales sans fuite d'info (`{\"status\":…}`), court-circuit TOTAL du pipeline : pas de session, pas de rate-limit (un kubelet throttlé = cascade de restarts), pas de log par sonde."
283
+ }),
284
+ livenessPath: z.string().default("/livez").describe("Chemin de la probe liveness (k8s `livenessProbe.httpGet.path`). Répond 200 tant que le process sert — Y COMPRIS pendant le drain (un restart pendant le drain casserait le graceful shutdown)."),
285
+ readinessPath: z.string().default("/readyz").describe("Chemin de la probe readiness (k8s `readinessProbe.httpGet.path`). 200 quand le boot est complet (`onPostReady`) ; 503 avant, et dès le début du shutdown (le LB retire le pod AVANT le drain)."),
286
+ shutdownDelay: z.number().int().nonnegative().default(0).describe("Délai (ms) entre la bascule readiness→503 et le début du drain — laisse aux endpoints k8s/LB le temps de retirer le pod (fenêtre réelle de propagation ~1-2 s). 0 (défaut) = drain immédiat ; alternative opérationnelle : `lifecycle.preStop: sleep N` dans le pod spec. À additionner au budget < grace period (30 s k8s).")
287
+ }).describe("Probes de santé cloud-native `/livez` + `/readyz` (liveness/readiness k8s, HEALTHCHECK Docker) — servies par les serveurs HTTP et HTTPS.");
288
+ const httpConfigSchema = z.strictObject({
289
+ headerServer: z.string().nullable().default("nodefony").meta({
290
+ runtimeMutable: true,
291
+ description: "Valeur de l'en-tête `Server:`. `null` = ne pas exposer l'identité du serveur (recommandé en production). Relu à chaque requête (éditable à chaud)."
292
+ }),
293
+ trustProxy: z.union([
294
+ z.boolean(),
295
+ z.string(),
296
+ z.array(z.string())
297
+ ]).default(false).describe("Confiance envers les en-têtes `X-Forwarded-*` (RFC 7239). `false` (DÉFAUT, secure) = ignorés. `true` = confiance totale (UNIQUEMENT si reverse-proxy unique point d'entrée). IP/CIDR/liste/preset (`loopback`/`linklocal`/`uniquelocal`) = confiance conditionnelle au socket."),
298
+ trustedHosts: z.union([
299
+ z.boolean(),
300
+ z.string(),
301
+ z.array(z.string())
302
+ ]).default(false).describe("Barrière `Host` testée AVANT le routing (anti Host-header injection). Le domaine canonique (`kernel.domain`) est TOUJOURS accepté, + le loopback (`localhost`/`127.0.0.1`/`[::1]`) en development. `false` (DÉFAUT) = ce socle seul. `true` = bypass (tout `Host` passe — quand un reverse-proxy filtre déjà le `Host`, cf cloud-native). string/liste = vhosts additionnels : exact (`marseille.fr`) ou wildcard un-label (`*.cdn.example.com`). N.B. : ce n'est PAS la liste des vhosts servis (ça, c'est `@Domain` sur les contrôleurs) — juste la barrière sécu."),
303
+ securityHeaders: securityHeadersSchema.default(() => securityHeadersSchema.parse({})),
304
+ maxBodySize: z.number().int().nonnegative().default(1048576).meta({
305
+ runtimeMutable: true,
306
+ description: "Taille max (octets) d'un corps de requête NON-multipart (JSON, urlencoded, XML, brut) avant rejet RFC 9110 413 « Content Too Large ». Vérifiée d'abord sur `Content-Length` (rejet avant lecture), puis en continu pendant le streaming (anti chunked/Content-Length menteur). Défaut 1 MiB (secure-by-default, anti DoS mémoire). 0 = illimité. Lu à chaque requête (éditable à chaud). Le multipart a ses propres limites (`upload.maxFileSize`/`maxTotalFileSize`)."
307
+ }),
308
+ upload: uploadSchema.default(() => uploadSchema.parse({})),
309
+ queryString: queryStringSchema.default(() => queryStringSchema.parse({})),
310
+ http: httpServerSchema.default(() => httpServerSchema.parse({})),
311
+ https: httpsServerSchema.default(() => httpsServerSchema.parse({})),
312
+ http2: http2Schema.default(() => http2Schema.parse({})),
313
+ http3: http3Schema.default(() => http3Schema.parse({})).meta({
314
+ reserved: true,
315
+ description: "RÉSERVÉ — serveur HTTP/3 (QUIC), nécessitera Node.js >= 28."
316
+ }),
317
+ certificates: certificatesSchema.default(() => certificatesSchema.parse({})),
318
+ websocket: websocketSchema.default(() => websocketSchema.parse({})),
319
+ websocketSecure: websocketSchema.default(() => websocketSchema.parse({})).describe("Serveur WebSocket Secure (wss://, port 5152). Même forme que `websocket`."),
320
+ statics: staticsSchema.default(() => staticsSchema.parse({})),
321
+ session: sessionSchema.default(() => sessionSchema.parse({})),
322
+ rateLimit: rateLimitSchema.default(() => rateLimitSchema.parse({})),
323
+ health: healthSchema.default(() => healthSchema.parse({})),
324
+ wsMaxConnectionsPerIp: z.number().int().positive().nullable().default(null).meta({
325
+ runtimeMutable: true,
326
+ description: "Backstop OPT-IN : plafond de connexions WebSocket CONCURRENTES par IP cliente. `null` (DÉFAUT) = désactivé. Distinct de `rateLimit` (qui borne le DÉBIT d'ouverture par fenêtre) : ici on borne le NOMBRE de sockets simultanément ouvertes par IP. Au-delà, l'upgrade est fermé (RFC 6455 close 1013). ⚠️ PORTÉE PAR PROCESS (1 pod) : un vrai plafond GLOBAL/IP se fait à l'INGRESS/LB — nginx `limit_conn`, HAProxy `sc_conn_cur`, annotation k8s `nginx.ingress.kubernetes.io/limit-connections` — qui voit TOUT le trafic, rejette AVANT que l'app paie le fd + le handshake TLS, et couvre tous les pods. En cloud-native, laisser `null` et déléguer à l'edge. N'activer (ex. 20) que sur bare-metal/VPS SANS ingress, comme défense en profondeur. IP résolue forwarded-aware (RFC 7239 + trustProxy)."
327
+ })
328
+ }).describe("Configuration de @nodefony/http.");
329
+ /**
330
+ * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
331
+ * valides par construction ; passés au `super(..., config)` du Module class.
332
+ */
333
+ const config = httpConfigSchema.parse({});
334
+ //#endregion
335
+ export { config as default, httpConfigSchema };
@@ -0,0 +1,93 @@
1
+ import { httpConfigSchema } from "./config.js";
2
+ import { parseModuleConfig } from "nodefony";
3
+ import { z } from "zod";
4
+ //#region nodefony/config/defineModuleConfig.ts
5
+ /**
6
+ * Injecte les défauts dérivés du kernel APRÈS le parse (le schéma reste pur).
7
+ *
8
+ * - `upload.uploadDir` vide → répertoire temporaire du kernel (`kernel.tmpDir`).
9
+ * - `certificates.openssl.attrs` vide → sujet du certificat dérivé du kernel
10
+ * (`commonName` ← `kernel.domain`, `organizationName` ← `kernel.projectName`).
11
+ * Sans ce remplissage, le certificat auto-signé n'aurait pas de commonName.
12
+ * - `certificates.san` vide → Subject Alternative Name dérivé du kernel
13
+ * (`localhost` + `kernel.domain` en DNS ; `127.0.0.1`/`::1` en IP). Le SAN fait
14
+ * foi pour la vérification d'hôte (RFC 6125) — Chrome ignore le commonName.
15
+ * `0.0.0.0` (bind toutes interfaces) n'est PAS un nom DNS valide → exclu.
16
+ *
17
+ * Muté EN PLACE (pas de copie) : la config est ré-assignée à `module.options`
18
+ * dans `onKernelRegister`, AVANT que les `@services` (qui lisent et mutent
19
+ * `module.options` — ex. `uploadDir`, `serialNumber`) ne soient instanciés à
20
+ * `onBoot`. C'est pourquoi la config N'EST PAS gelée (≠ `@nodefony/redis`).
21
+ *
22
+ * @param config - config déjà parsée (sortie du schéma).
23
+ * @param kernel - kernel courant (présent à `onKernelRegister`).
24
+ * @returns la même config, complétée.
25
+ */
26
+ function applyKernelDefaults(config, kernel) {
27
+ if (!config.upload.uploadDir) {
28
+ const p = kernel?.tmpDir?.path;
29
+ config.upload.uploadDir = typeof p === "string" ? p : "/tmp";
30
+ }
31
+ if (config.certificates.openssl.attrs.length === 0) config.certificates.openssl.attrs = [
32
+ {
33
+ name: "commonName",
34
+ value: kernel?.domain ?? "nodefony.com"
35
+ },
36
+ {
37
+ name: "organizationName",
38
+ value: kernel?.projectName ?? ""
39
+ },
40
+ {
41
+ name: "organizationalUnitName",
42
+ value: "Development"
43
+ }
44
+ ];
45
+ const san = config.certificates.san;
46
+ if (san.dns.length === 0 && san.ip.length === 0) {
47
+ const domain = kernel?.domain;
48
+ const dns = ["localhost"];
49
+ const ip = ["127.0.0.1", "::1"];
50
+ if (domain && domain !== "localhost" && domain !== "0.0.0.0") {
51
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(domain) || domain.includes(":")) {
52
+ if (!ip.includes(domain)) ip.unshift(domain);
53
+ } else dns.unshift(domain);
54
+ }
55
+ config.certificates.san = {
56
+ dns,
57
+ ip
58
+ };
59
+ }
60
+ return config;
61
+ }
62
+ /**
63
+ * Builder type-safe de la configuration de `@nodefony/http`.
64
+ *
65
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
66
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
67
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
68
+ * env + freeze) et publie le JSON Schema Studio.
69
+ *
70
+ * Aligné sur `defineRedisConfig` / `defineSecurityConfig` : source unique =
71
+ * `./config.ts` (Zod) ; le builder VALIDE puis complète les défauts kernel.
72
+ *
73
+ * @param config - configuration brute (sections omises = défauts sûrs).
74
+ * @param kernel - kernel courant, pour les défauts dérivés (tmpDir, domain).
75
+ * @returns config validée et complétée (NON gelée — les services la mutent).
76
+ * @throws BootConfigurationError si un champ est invalide ou une clé inconnue —
77
+ * `parseModuleConfig` (cœur) porte le message ET le type d'erreur, pour que le
78
+ * refus interrompe le boot en développement comme en production.
79
+ */
80
+ function defineHttpConfig(config = {}, kernel) {
81
+ const parsed = parseModuleConfig(httpConfigSchema, config, "@nodefony/http");
82
+ if (kernel?.environment === "production" && config.headerServer === void 0) parsed.headerServer = null;
83
+ return applyKernelDefaults(parsed, kernel);
84
+ }
85
+ /**
86
+ * JSON Schema introspectable de la config HTTP — destiné au formulaire
87
+ * d'édition Studio (futur) et à la documentation générée.
88
+ */
89
+ function httpConfigJsonSchema() {
90
+ return z.toJSONSchema(httpConfigSchema);
91
+ }
92
+ //#endregion
93
+ export { defineHttpConfig, httpConfigJsonSchema };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};