@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,72 @@
1
+ import { severityFromStatus } from "./audit-logger.js";
2
+ import { logColor } from "nodefony";
3
+ import { performance } from "node:perf_hooks";
4
+ //#region nodefony/service/pretty-request-logger.ts
5
+ /**
6
+ * Pretty single-line logger for dev (P3.2) — the biggest gain for humans.
7
+ *
8
+ * Format (no padding, ANSI-colored):
9
+ * `GET 200 /api/test 12.3ms 127.0.0.1 [a1b2c3d4]`
10
+ *
11
+ * - method : cyan
12
+ * - status : green (2xx) / yellow (3xx) / yellow-bold (4xx) / red (5xx)
13
+ * - url : default
14
+ * - duration : dim
15
+ * - remote : dim
16
+ * - requestId : magenta-dim, truncated to first 8 chars
17
+ *
18
+ * Activate with `httpKernel.setRequestLogger(new PrettyRequestLogger())`.
19
+ *
20
+ * Stateless singleton. Allocates a few strings per request (terminal log
21
+ * path) — zero overhead when not activated.
22
+ */
23
+ var PrettyRequestLogger = class {
24
+ renderHttp(context, error) {
25
+ const ctx = context;
26
+ const err = error ?? ctx.error ?? null;
27
+ const status = ctx.response?.statusCode ?? (err ? 500 : null);
28
+ const method = ctx.method ?? "?";
29
+ const duration = computeDurationMs(ctx.phases);
30
+ return {
31
+ text: `${logColor.cyan(method.padEnd(4))} ${colorizeStatus(status)} ${ctx.url} ${logColor.blackBright(formatDuration(duration))} ${logColor.blackBright(ctx.remoteAddress ?? "-")} ${logColor.magenta(`[${shortId(ctx.requestId)}]`)}` + (err ? ` ${logColor.red(err.message)}` : ""),
32
+ severity: severityFromStatus(status),
33
+ msgid: "req"
34
+ };
35
+ }
36
+ renderWebsocket(context, error, acceptedProtocol) {
37
+ const ctx = context;
38
+ const status = ctx.response?.statusCode ?? (error ? 500 : null);
39
+ const duration = computeDurationMs(ctx.phases);
40
+ const proto = acceptedProtocol ? `[${acceptedProtocol}]` : "";
41
+ return {
42
+ text: `${logColor.cyan("WS ")} ${colorizeStatus(status)} ${ctx.url} ${logColor.blackBright(proto)} ${logColor.blackBright(formatDuration(duration))} ${logColor.blackBright(ctx.remoteAddress ?? "-")} ${logColor.magenta(`[${shortId(ctx.requestId)}]`)}` + (error ? ` ${logColor.red(error.message)}` : ""),
43
+ severity: error ? "ERROR" : "INFO",
44
+ msgid: "req"
45
+ };
46
+ }
47
+ };
48
+ function colorizeStatus(status) {
49
+ if (status === null) return logColor.blackBright("---");
50
+ const s = String(status);
51
+ if (status >= 500) return logColor.red(s);
52
+ if (status >= 400) return logColor.yellowBold(s);
53
+ if (status >= 300) return logColor.yellow(s);
54
+ return logColor.green(s);
55
+ }
56
+ function formatDuration(ms) {
57
+ if (ms === null) return "---";
58
+ if (ms < 1) return `${ms.toFixed(2)}ms`;
59
+ if (ms < 1e3) return `${ms.toFixed(1)}ms`;
60
+ return `${(ms / 1e3).toFixed(2)}s`;
61
+ }
62
+ function shortId(id) {
63
+ return id.length > 8 ? id.slice(0, 8) : id;
64
+ }
65
+ function computeDurationMs(phases) {
66
+ if (!phases.length) return null;
67
+ const first = phases[0];
68
+ if (typeof first.startMs !== "number") return null;
69
+ return performance.now() - first.startMs;
70
+ }
71
+ //#endregion
72
+ export { PrettyRequestLogger as default };
@@ -0,0 +1,54 @@
1
+ import { logColor } from "nodefony";
2
+ //#region nodefony/service/request-logger.ts
3
+ /**
4
+ * Default Nodefony per-request logger — preserves the legacy colored format.
5
+ *
6
+ * HTTP success: `URL : <url> FROM : <remote> ORIGIN : <host> ID : <uuid>`
7
+ * HTTP error: adds the error toString (in dev), single-line in prod
8
+ * WebSocket: adds `Accept-Protocol : <proto>` on success
9
+ *
10
+ * Stateless singleton — zero per-request allocation. Override via
11
+ * `httpKernel.setRequestLogger(custom)` for JSON access logs (P3.1),
12
+ * pretty formatter (P3.2), or NCSA combined format (P3.10).
13
+ */
14
+ var DefaultRequestLogger = class {
15
+ renderHttp(context, error) {
16
+ const ctx = context;
17
+ const txt = `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}`;
18
+ const err = error ?? ctx.error;
19
+ if (err) {
20
+ const errCode = err.code ?? ctx.response?.statusCode ?? 500;
21
+ const msgid = `${ctx.type} ${logColor.magenta(errCode)} ${logColor.red(ctx.method ?? "")}`;
22
+ return {
23
+ text: ctx.kernel?.environment === "production" ? `${txt} ${err}` : `${txt}\n ${err}`,
24
+ severity: "ERROR",
25
+ msgid
26
+ };
27
+ }
28
+ return {
29
+ text: txt,
30
+ severity: "INFO",
31
+ msgid: `${ctx.type} ${logColor.magenta(ctx.response?.statusCode ?? "")} ${ctx.method}`
32
+ };
33
+ }
34
+ renderWebsocket(context, error, acceptedProtocol) {
35
+ const ctx = context;
36
+ if (error) {
37
+ const errCode = error.code ?? ctx.response?.statusCode ?? 500;
38
+ const msgid = `${ctx.type} ${logColor.magenta(errCode)} ${logColor.red(ctx.method ?? "")}`;
39
+ return {
40
+ text: `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}\n ` + error.toString(),
41
+ severity: "ERROR",
42
+ msgid
43
+ };
44
+ }
45
+ const msgid = `${ctx.type} ${logColor.magenta(ctx.response?.statusCode ?? "")} ${ctx.method}`;
46
+ return {
47
+ text: `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("Accept-Protocol")} : ${acceptedProtocol || "*"} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}`,
48
+ severity: "INFO",
49
+ msgid
50
+ };
51
+ }
52
+ };
53
+ //#endregion
54
+ export { DefaultRequestLogger as default };
@@ -0,0 +1,20 @@
1
+ //#region nodefony/service/servers/clientError.ts
2
+ /**
3
+ * Gère l'event `clientError` d'un `http.Server` / `https.Server`.
4
+ *
5
+ * ⚠️ Doc Node (`http.Server` event `'clientError'`) : dès qu'un listener
6
+ * `clientError` est attaché, Node **cesse de fermer le socket automatiquement**
7
+ * (le défaut aurait répondu `400` + détruit le socket). Sans fermeture
8
+ * explicite, un socket malformé reste ouvert → fuite de socket/FD = DoS sur
9
+ * requête mal formée. On répond donc une réponse minimale et on ferme.
10
+ *
11
+ * @param error - erreur émise (codes `llhttp` : `HPE_*`, ou `ECONNRESET`…).
12
+ * @param socket - flux client brut (l'event `clientError` fournit un `Duplex`).
13
+ */
14
+ function handleClientError(error, socket) {
15
+ if (error?.code === "ECONNRESET" || !socket.writable) return;
16
+ const statusLine = error?.code === "HPE_HEADER_OVERFLOW" ? "431 Request Header Fields Too Large" : "400 Bad Request";
17
+ socket.end(`HTTP/1.1 ${statusLine}\r\nConnection: close\r\n\r\n`);
18
+ }
19
+ //#endregion
20
+ export { handleClientError };
@@ -0,0 +1,135 @@
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
3
+ import http_kernel_default from "../http-kernel.js";
4
+ import { handleClientError } from "./clientError.js";
5
+ import { createDrainTerminator } from "./serverShutdown.js";
6
+ import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
7
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
8
+ import { Module, Service, extend, inject, nodefonyError } from "nodefony";
9
+ import http from "node:http";
10
+ //#region nodefony/service/servers/server-http.ts
11
+ let ServerHttp = class ServerHttp extends Service {
12
+ httpKernel;
13
+ module;
14
+ server = null;
15
+ httpTerminator = null;
16
+ port;
17
+ protocol = "1.1";
18
+ ready = false;
19
+ type = "http";
20
+ domain;
21
+ scheme = "http";
22
+ address = null;
23
+ family = null;
24
+ active = false;
25
+ infos = null;
26
+ constructor(module, httpKernel) {
27
+ super("server-http", module.container, module.notificationsCenter, module.options.http);
28
+ this.httpKernel = httpKernel;
29
+ this.module = module;
30
+ this.active = !!module.kernel?.options.servers.http;
31
+ this.port = this.setPort();
32
+ this.domain = this.module.kernel?.domain;
33
+ }
34
+ setPort() {
35
+ if (this.module.kernel?.options.servers?.http) return this.module.kernel?.options.servers?.http?.port || 0;
36
+ return 0;
37
+ }
38
+ createServer() {
39
+ return new Promise((resolve, reject) => {
40
+ try {
41
+ if (!this.active) return reject(/* @__PURE__ */ new Error(`Server http is not allowed in config file `));
42
+ const opt = extend({ requestTimeout: this.options.requestTimeout });
43
+ this.server = http.createServer(opt);
44
+ this.httpTerminator = createDrainTerminator(this.server, this.options.shutdownTimeout);
45
+ if (this.options.maxHeadersCount) {
46
+ if (this.server) this.server.maxHeadersCount = this.options.maxHeadersCount;
47
+ }
48
+ if (this.options.timeout) {
49
+ if (this.server) {
50
+ this.server.setTimeout(this.options.timeout, () => {
51
+ this.fire("onTimeout", this);
52
+ });
53
+ this.server.timeout = this.options.timeout;
54
+ }
55
+ }
56
+ if (this.options.keepAliveTimeout) {
57
+ if (this.server) this.server.keepAliveTimeout = this.options.keepAliveTimeout;
58
+ }
59
+ this.server.on("request", (request, response) => this.httpKernel?.onHttpRequest(request, response, this.type).catch(() => {}));
60
+ this.module.fire("onCreateServer", this.type, this);
61
+ bindWithFallback(this.server, this.domain, buildBindPlan("http", this.module.kernel?.options.servers, this.module.kernel?.environment)).then(({ address, shiftedFrom }) => {
62
+ this.infos = address;
63
+ this.port = address.port;
64
+ this.address = address.address;
65
+ this.family = address.family;
66
+ if (shiftedFrom !== null) this.log(`Port ${shiftedFrom} déjà occupé → HTTP écoute sur ${this.port}. Figer le port : servers.http.port ; échouer au lieu de glisser : servers.portPolicy = "strict".`, "WARNING");
67
+ this.ready = true;
68
+ this.module.fire("onServersReady", this.type, this);
69
+ this.attachErrorHandler();
70
+ resolve(this.server);
71
+ }).catch((error) => {
72
+ this.reportBindError(error);
73
+ reject(error);
74
+ });
75
+ this.kernel?.once("onTerminate", async () => {
76
+ if (!this.server) return;
77
+ try {
78
+ await this.httpTerminator?.terminate();
79
+ this.log(`${this.type} SHUTDOWN Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
80
+ } catch (e) {
81
+ this.log(e, "ERROR", "TERMINATE");
82
+ }
83
+ });
84
+ this.server.on("clientError", (e, socket) => {
85
+ this.fire("onClientError", e, socket);
86
+ handleClientError(e, socket);
87
+ });
88
+ } catch (e) {
89
+ this.log(e, "CRITIC");
90
+ return reject(e);
91
+ }
92
+ });
93
+ }
94
+ /**
95
+ * Handler d'erreur DURABLE — posé une fois le serveur en écoute.
96
+ *
97
+ * Il ne traite donc plus `EADDRINUSE` au bind (le binder s'en charge, avec ou
98
+ * sans repli) : il couvre les erreurs de la VIE du serveur.
99
+ */
100
+ attachErrorHandler() {
101
+ this.server?.on("error", (error) => {
102
+ this.log(new nodefonyError(error), "CRITIC");
103
+ });
104
+ }
105
+ /**
106
+ * Le bind a échoué pour de bon — soit `portPolicy: "strict"`, soit tous les
107
+ * ports de repli étaient pris, soit une erreur qui n'est pas un conflit de port.
108
+ *
109
+ * Reste FATAL (contrat inchangé) : un serveur qui n'écoute pas ne doit jamais
110
+ * laisser le process traîner en se croyant démarré.
111
+ */
112
+ reportBindError(error) {
113
+ const myError = new nodefonyError(error);
114
+ switch (error.code) {
115
+ case "ENOTFOUND":
116
+ this.log(`CHECK DOMAIN IN /etc/hosts or config unable to connect to : ${this.domain}`, "ERROR");
117
+ this.log(myError, "CRITIC");
118
+ break;
119
+ case "EADDRINUSE":
120
+ this.log(`Port ${this.port} déjà occupé (domaine ${this.domain}) — le serveur HTTP ne peut pas écouter. Libérer le port, en choisir un autre (servers.http.port), ou autoriser le repli automatique (servers.portPolicy = "auto", défaut en développement).`, "ERROR");
121
+ this.log(myError, "CRITIC");
122
+ break;
123
+ default: this.log(myError, "CRITIC");
124
+ }
125
+ this.server?.close();
126
+ setTimeout(() => this.kernel?.terminate(1), 1e3);
127
+ }
128
+ showBanner() {
129
+ if (this.infos) this.log(`Server Listen on ${this.scheme}://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
130
+ }
131
+ };
132
+ ServerHttp = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], ServerHttp);
133
+ var server_http_default = ServerHttp;
134
+ //#endregion
135
+ export { server_http_default as default };
@@ -0,0 +1,204 @@
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
3
+ import http_kernel_default from "../http-kernel.js";
4
+ import { handleClientError } from "./clientError.js";
5
+ import { createDrainTerminator } from "./serverShutdown.js";
6
+ import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
7
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
8
+ import { Module, Service, extend, inject, nodefonyError } from "nodefony";
9
+ import http2 from "node:http2";
10
+ import https from "node:https";
11
+ import { TLSSocket } from "node:tls";
12
+ //#region nodefony/service/servers/server-https.ts
13
+ let ServerHttps = class ServerHttps extends Service {
14
+ httpKernel;
15
+ httpTerminator = null;
16
+ module;
17
+ server = null;
18
+ port;
19
+ protocol = "1.1";
20
+ ready = false;
21
+ type = "https";
22
+ domain;
23
+ scheme = "https";
24
+ address = null;
25
+ family = null;
26
+ active = false;
27
+ infos = null;
28
+ constructor(module, httpKernel) {
29
+ super("server-https", module.container, module.notificationsCenter, module.options.https);
30
+ this.httpKernel = httpKernel;
31
+ this.module = module;
32
+ this.active = !!this.kernel?.options.servers.https;
33
+ this.port = this.setPort();
34
+ this.domain = this.kernel?.domain;
35
+ }
36
+ terminator() {
37
+ if (this.server) return createDrainTerminator(this.server, this.options.shutdownTimeout);
38
+ throw new Error(`Server not found`);
39
+ }
40
+ setPort() {
41
+ if (this.kernel?.options.servers?.https) return this.kernel?.options.servers?.https?.port || 0;
42
+ return 0;
43
+ }
44
+ createServer() {
45
+ return new Promise((resolve, reject) => {
46
+ try {
47
+ if (!this.active) return reject(/* @__PURE__ */ new Error(`Server https is not allowed in config file `));
48
+ this.protocol = this.kernel?.options.servers.https.protocol;
49
+ if (this.protocol === "2.0") return resolve(this.createServerH2());
50
+ const opt = extend({
51
+ requestTimeout: this.options.requestTimeout,
52
+ rejectUnauthorized: this.options.rejectUnauthorized,
53
+ key: this.httpKernel?.serviceCerticats?.key,
54
+ cert: this.httpKernel?.serviceCerticats?.cert,
55
+ ca: this.httpKernel?.serviceCerticats?.ca ? this.httpKernel?.serviceCerticats?.ca : void 0
56
+ });
57
+ this.server = https.createServer(opt);
58
+ this.httpTerminator = this.terminator();
59
+ if (this.options.timeout) {
60
+ if (this.server) {
61
+ this.server.setTimeout(this.options.timeout, () => {
62
+ this.fire("onTimeout", this);
63
+ });
64
+ this.server.timeout = this.options.timeout;
65
+ }
66
+ }
67
+ if (this.options.maxHeadersCount) {
68
+ if (this.server) this.server.maxHeadersCount = this.options.maxHeadersCount;
69
+ }
70
+ if (this.options.keepAliveTimeout) {
71
+ if (this.server) this.server.keepAliveTimeout = this.options.keepAliveTimeout;
72
+ }
73
+ this.module.fire("onCreateServer", this.type, this);
74
+ this.listenWithPolicy().then(() => resolve(this.server)).catch((error) => {
75
+ this.reportBindError(error);
76
+ reject(error);
77
+ });
78
+ this.server.on("request", (request, response) => {
79
+ this.httpKernel?.onHttpRequest(request, response, this.type).catch(() => {});
80
+ });
81
+ this.kernel?.once("onTerminate", async () => {
82
+ if (!this.server) return;
83
+ try {
84
+ await this.httpTerminator?.terminate();
85
+ this.log(`${this.type} SHUTDOWN Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
86
+ } catch (e) {
87
+ this.log(e, "ERROR", "TERMINATE");
88
+ }
89
+ });
90
+ this.server.on("clientError", (e, socket) => {
91
+ this.fire("onClientError", e, socket);
92
+ handleClientError(e, socket);
93
+ });
94
+ } catch (e) {
95
+ this.log(e, "CRITIC");
96
+ return reject(e);
97
+ }
98
+ });
99
+ }
100
+ createServerH2() {
101
+ return new Promise((resolve, reject) => {
102
+ try {
103
+ const h2Cfg = (this.module?.options)?.http2 ?? {};
104
+ const opt = extend({
105
+ allowHTTP1: true,
106
+ rejectUnauthorized: this.options.rejectUnauthorized,
107
+ key: this.httpKernel?.serviceCerticats?.key,
108
+ cert: this.httpKernel?.serviceCerticats?.cert,
109
+ ca: this.httpKernel?.serviceCerticats?.ca ? this.httpKernel?.serviceCerticats?.ca : void 0
110
+ });
111
+ if (h2Cfg.maxSessionMemory) opt.maxSessionMemory = h2Cfg.maxSessionMemory;
112
+ if (h2Cfg.maxConcurrentStreams) opt.settings = {
113
+ ...opt.settings,
114
+ maxConcurrentStreams: h2Cfg.maxConcurrentStreams
115
+ };
116
+ this.server = http2.createSecureServer(opt);
117
+ this.httpTerminator = this.terminator();
118
+ if (this.options.timeout) this.server.setTimeout(this.options.timeout, () => {
119
+ this.fire("onTimeout", this);
120
+ });
121
+ this.server.on("request", (request, response) => {
122
+ let alpnProtocol = false;
123
+ if (request && request.stream && request.stream.session?.socket) {
124
+ if (request.stream.session.socket instanceof TLSSocket) alpnProtocol = request.stream.session.socket.alpnProtocol;
125
+ }
126
+ if (request.socket) {
127
+ if (request.socket instanceof TLSSocket) alpnProtocol = request.socket.alpnProtocol;
128
+ }
129
+ if (alpnProtocol === "h2") return this.httpKernel?.onHttpRequest(request, response, "http2").catch(() => {});
130
+ else return this.httpKernel?.onHttpRequest(request, response, "https").catch(() => {});
131
+ });
132
+ this.listenWithPolicy().then(() => resolve(this.server)).catch((error) => {
133
+ this.reportBindError(error);
134
+ reject(error);
135
+ });
136
+ this.kernel?.once("onTerminate", async () => {
137
+ if (!this.server) return;
138
+ try {
139
+ await this.httpTerminator?.terminate();
140
+ this.log(`${this.type} SHUTDOWN Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
141
+ } catch (e) {
142
+ this.log(e, "ERROR", "TERMINATE");
143
+ }
144
+ });
145
+ this.server.on("sessionError", (error) => {
146
+ this.log(error, "ERROR", "HTTP2 Server sessionError");
147
+ });
148
+ this.server.on("streamError", (error) => {
149
+ this.log(error, "ERROR", "HTTP2 Server streamError");
150
+ });
151
+ } catch (e) {
152
+ return reject(e);
153
+ }
154
+ });
155
+ }
156
+ /**
157
+ * Écoute selon `servers.portPolicy`, commune aux deux branches TLS (HTTP/1.1 et
158
+ * HTTP/2) — leur seule différence est le type du serveur, pas la politique.
159
+ *
160
+ * Pose le handler d'erreur durable une fois en écoute (donc APRÈS les éventuels
161
+ * `EADDRINUSE` de repli, qui ne sont pas des pannes).
162
+ */
163
+ async listenWithPolicy() {
164
+ const { address, shiftedFrom } = await bindWithFallback(this.server, this.domain, buildBindPlan("https", this.module.kernel?.options.servers, this.module.kernel?.environment));
165
+ this.infos = address;
166
+ this.port = address.port;
167
+ this.address = address.address;
168
+ this.family = address.family;
169
+ if (shiftedFrom !== null) this.log(`Port ${shiftedFrom} déjà occupé → HTTPS écoute sur ${this.port}. Figer le port : servers.https.port ; échouer au lieu de glisser : servers.portPolicy = "strict".`, "WARNING");
170
+ this.ready = true;
171
+ this.module.fire("onServersReady", this.type, this);
172
+ this.attachErrorHandler();
173
+ }
174
+ /** Erreurs de la VIE du serveur (le bind est déjà passé — cf server-http.ts). */
175
+ attachErrorHandler() {
176
+ this.server?.on("error", (error) => {
177
+ this.log(new nodefonyError(error), "CRITIC");
178
+ });
179
+ }
180
+ /** Bind définitivement impossible → FATAL (contrat inchangé). */
181
+ reportBindError(error) {
182
+ const myError = new nodefonyError(error);
183
+ switch (error.code) {
184
+ case "ENOTFOUND":
185
+ this.log(`CHECK DOMAIN IN /etc/hosts or config unable to connect to : ${this.domain}`, "ERROR");
186
+ this.log(myError, "CRITIC");
187
+ break;
188
+ case "EADDRINUSE":
189
+ this.log(`Port ${this.port} déjà occupé (domaine ${this.domain}) — le serveur HTTPS ne peut pas écouter. Libérer le port, en choisir un autre (servers.https.port), ou autoriser le repli automatique (servers.portPolicy = "auto", défaut en développement).`, "ERROR");
190
+ this.log(myError, "CRITIC");
191
+ break;
192
+ default: this.log(myError, "CRITIC");
193
+ }
194
+ this.server?.close();
195
+ setTimeout(() => this.kernel?.terminate(1), 1e3);
196
+ }
197
+ showBanner() {
198
+ if (this.infos) this.log(`Server Listen on ${this.scheme}://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
199
+ }
200
+ };
201
+ ServerHttps = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], ServerHttps);
202
+ var server_https_default = ServerHttps;
203
+ //#endregion
204
+ export { server_https_default as default };
@@ -0,0 +1,192 @@
1
+ import { Service, extend } from "nodefony";
2
+ import { URL } from "node:url";
3
+ import http from "node:http";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import mime from "mime-types";
7
+ import http2 from "node:http2";
8
+ import tls from "node:tls";
9
+ import serveStatic from "serve-static";
10
+ //#region nodefony/service/servers/server-static.ts
11
+ const defaultOptions = {
12
+ cacheControl: true,
13
+ maxAge: 345600
14
+ };
15
+ var Statics = class extends Service {
16
+ module;
17
+ servers;
18
+ /** Montages préfixés (prod frontend). Lazy : `[]` rempli à `addMount`. */
19
+ mounts = [];
20
+ /**
21
+ * Serveur statique config-driven actif. `false` (config `statics.enabled`) =
22
+ * aucun montage `web`/`assets`, 0 listener — quand un reverse-proxy/CDN sert
23
+ * les statiques. Ne gate PAS les `addMount()` programmatiques (frontend prod).
24
+ */
25
+ enabled = true;
26
+ defaultOptions = defaultOptions;
27
+ constructor(module) {
28
+ const container = module.container || void 0;
29
+ const options = module.options.statics || {};
30
+ let event;
31
+ if (container) event = container.get("notificationsCenter");
32
+ super("server-static", container, event, options);
33
+ this.module = module;
34
+ this.servers = {};
35
+ this.enabled = this.options.enabled !== false;
36
+ if (typeof this.options.enabled !== "undefined") delete this.options.enabled;
37
+ this.defaultOptions = extend(defaultOptions, this.options.defaultOptions || {});
38
+ if (this.options.defaultOptions) delete this.options.defaultOptions;
39
+ if (this.enabled) this.initStaticFiles();
40
+ else this.log("Static file server DISABLED (statics.enabled=false) — reverse-proxy/CDN attendu", "INFO");
41
+ this.kernel?.once("onReady", () => {
42
+ this.mountModulePublics();
43
+ });
44
+ this.kernel?.on("onPostReady", () => {
45
+ if (this.kernel?.suppressBootBanners) return;
46
+ for (const ele in this.servers) this.log(`Server Listen on ${ele}`, "INFO");
47
+ });
48
+ }
49
+ /**
50
+ * Auto-monte le `public/` de chaque module applicatif sous le préfixe natif
51
+ * `/<module>/` (basename du nom — `@nodefony/test` → `/test/`). Appelé une fois
52
+ * à `onReady`. Idempotent via {@link addMount} (un même préfixe est remplacé).
53
+ *
54
+ * Exclusions :
55
+ * - **app root** (`isApp`) — son `public/` est servi à la racine `/` par la
56
+ * racine statique `web` (favicon…), sans préfixe.
57
+ * - **modules frontend-managed** (présents dans `frontend.listEntries()`) —
58
+ * leurs assets buildés sont servis sous `/_assets/<name>/` par
59
+ * `@nodefony/frontend` ; on ne double-sert pas leur `public/dist`.
60
+ * - **modules sans `public/`** — rien à monter (http, framework, security…).
61
+ *
62
+ * Enregistré dans {@link mounts} quel que soit `enabled` → la carte
63
+ * préfixe→dossier reste lisible par `proxy:generate` même statiques désactivés
64
+ * (reverse-proxy/CDN en prod).
65
+ */
66
+ mountModulePublics() {
67
+ const modules = this.kernel?.modules;
68
+ if (!modules) return;
69
+ const frontend = this.container?.get("frontend");
70
+ const frontManaged = frontend?.listEntries ? new Set(frontend.listEntries().map((e) => e.moduleName)) : null;
71
+ for (const name in modules) {
72
+ const mod = modules[name];
73
+ if (!mod || mod.isApp) continue;
74
+ const cfg = mod.options?.publicMount;
75
+ if (cfg === false) continue;
76
+ if (cfg == null && frontManaged?.has(mod.name)) continue;
77
+ const dir = join(mod.path, cfg?.dir || "public");
78
+ if (!existsSync(dir)) continue;
79
+ const prefix = cfg?.publicPath || `/${mod.name.split("/").pop()}/`;
80
+ this.addMount(prefix, dir);
81
+ }
82
+ }
83
+ initStaticFiles() {
84
+ for (const staticRoot in this.options) {
85
+ let Path = this.options[staticRoot].path;
86
+ Path = this.kernel?.checkPath(Path);
87
+ let setHeaders = null;
88
+ const opt = this.options[staticRoot].options || {};
89
+ if (opt.setHeaders) {
90
+ if (typeof opt.setHeaders === "function") {
91
+ setHeaders = opt.setHeaders;
92
+ delete opt.setHeaders;
93
+ }
94
+ }
95
+ opt.setHeaders = (res, path) => {
96
+ this.log(`Render ${path}`, "DEBUG", `SERVE STATIC ${staticRoot}`);
97
+ this.fire("onServeStatic", res, path, staticRoot, this);
98
+ };
99
+ if (setHeaders) this.on("onServeStatic", setHeaders);
100
+ this.addDirectory(Path, opt);
101
+ }
102
+ }
103
+ /**
104
+ * Monte un dossier sous un préfixe public (ex `/_assets/studio/` → outDir).
105
+ * Le préfixe est normalisé (leading + trailing `/`). Idempotent : un même
106
+ * préfixe est remplacé. Consommé par `@nodefony/frontend` en prod (assets
107
+ * Vite buildés) — résolu par nom via le Container, sans import croisé.
108
+ *
109
+ * @param prefix préfixe d'URL public
110
+ * @param dir dossier absolu à servir
111
+ */
112
+ addMount(prefix, dir) {
113
+ let p = prefix.trim();
114
+ if (!p.startsWith("/")) p = `/${p}`;
115
+ if (!p.endsWith("/")) p = `${p}/`;
116
+ p = p.replace(/\/{2,}/g, "/");
117
+ const server = serveStatic(dir, extend({}, this.defaultOptions));
118
+ const entry = {
119
+ prefix: p,
120
+ server,
121
+ dir
122
+ };
123
+ const i = this.mounts.findIndex((m) => m.prefix === p);
124
+ if (i >= 0) this.mounts[i] = entry;
125
+ else this.mounts.push(entry);
126
+ this.log(`mount ${p} → ${dir}`, "INFO");
127
+ }
128
+ /** `true` si au moins un montage préfixé est actif (gate du pipeline). */
129
+ hasMounts() {
130
+ return this.mounts.length > 0;
131
+ }
132
+ addDirectory(Path, options) {
133
+ if (!Path) throw new Error("Static file path not Defined ");
134
+ const opt = extend({}, this.defaultOptions, options);
135
+ const server = serveStatic(Path, opt);
136
+ this.servers[Path] = server;
137
+ return server;
138
+ }
139
+ getStatic(server, request, response) {
140
+ return new Promise((resolve, reject) => {
141
+ server(request, response, (err) => {
142
+ if (err) return reject(err);
143
+ return resolve(response);
144
+ });
145
+ });
146
+ }
147
+ getUrl(request) {
148
+ let scheme, host;
149
+ if (request instanceof http.IncomingMessage) {
150
+ scheme = request.connection instanceof tls.TLSSocket && request.connection.encrypted ? "https" : "http";
151
+ host = request.headers.host;
152
+ return scheme + "://" + host;
153
+ } else if (request instanceof http2.Http2ServerRequest) {
154
+ scheme = request.socket instanceof tls.TLSSocket && request.socket.encrypted ? "https" : "http";
155
+ host = request.headers[":authority"];
156
+ return scheme + "://" + host;
157
+ }
158
+ throw new Error(`Bad request type`);
159
+ }
160
+ async handle(request, response) {
161
+ if (!this.enabled && this.mounts.length === 0) return Promise.resolve(response);
162
+ const baseURL = this.getUrl(request);
163
+ const { pathname } = new URL(request.url, baseURL);
164
+ if (!pathname) throw new Error(`Bad url ${request.url}`);
165
+ if (this.mounts.length > 0) {
166
+ const raw = request.url;
167
+ for (const m of this.mounts) {
168
+ if (!raw.startsWith(m.prefix)) continue;
169
+ request.url = `/${raw.slice(m.prefix.length)}`;
170
+ try {
171
+ await this.getStatic(m.server, request, response);
172
+ request.url = raw;
173
+ } catch (e) {
174
+ request.url = raw;
175
+ return Promise.reject(e);
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ for (const server in this.servers) try {
181
+ let ele = this.servers[server];
182
+ await this.getStatic(ele, request, response);
183
+ const type = mime.lookup(pathname);
184
+ response.setHeader("Content-Type", type);
185
+ } catch (e) {
186
+ return Promise.reject(e);
187
+ }
188
+ return Promise.resolve(response);
189
+ }
190
+ };
191
+ //#endregion
192
+ export { Statics as default };