@nodefony/http 10.0.0-alpha.8 → 10.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -1
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +22 -1
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +25 -13
- package/dist/nodefony/service/error-renderer.js +118 -5
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +25 -17
- package/dist/nodefony/service/trace.js +1 -1
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +93 -44
- package/dist/nodefony/src/context/http/Response.js +107 -51
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +8 -21
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +10 -10
- package/dist/types/nodefony/service/error-renderer.d.ts +14 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/observabilite.md +30 -30
- package/docs/rate-limit.md +22 -22
- package/docs/servers.md +61 -61
- package/docs/session.md +82 -77
- package/docs/upload.md +43 -37
- package/package.json +11 -11
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import { isPortListening } from "nodefony";
|
|
1
|
+
import { isPortListening, resolvePortPolicy } from "nodefony";
|
|
2
2
|
//#region nodefony/src/servers/portBinder.ts
|
|
3
3
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
4
4
|
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
5
|
-
/**
|
|
6
|
-
* Politique de port effective.
|
|
7
|
-
*
|
|
8
|
-
* Le défaut dépend de l'environnement, et ce n'est pas de la coquetterie :
|
|
9
|
-
* - **production** → `strict`. Le port y est un contrat (service k8s, ingress,
|
|
10
|
-
* sonde de santé). Un bind silencieux ailleurs donnerait un pod déclaré sain
|
|
11
|
-
* que personne n'atteint : une panne invisible, le pire des deux mondes.
|
|
12
|
-
* - **test** → `strict`. Un port occupé veut dire « un serveur est resté debout » ;
|
|
13
|
-
* le banc doit s'arrêter, pas viser à côté (il taperait le serveur du voisin).
|
|
14
|
-
* - **développement** → `auto`. Ici un port pris n'est qu'une nuisance.
|
|
15
|
-
*
|
|
16
|
-
* @param environment - `kernel.environment` (normalisé `development`/`production`/`test`).
|
|
17
|
-
* @param explicit - `servers.portPolicy` s'il est configuré (il gagne toujours).
|
|
18
|
-
*/
|
|
19
|
-
function resolvePortPolicy(environment, explicit) {
|
|
20
|
-
if (explicit === "auto" || explicit === "strict") return explicit;
|
|
21
|
-
return environment === "development" ? "auto" : "strict";
|
|
22
|
-
}
|
|
23
5
|
/** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
|
|
24
6
|
function configuredPort(entry) {
|
|
25
7
|
if (!entry) return 0;
|
|
@@ -106,6 +88,8 @@ function portConflictError(port, host) {
|
|
|
106
88
|
error.code = "EADDRINUSE";
|
|
107
89
|
return error;
|
|
108
90
|
}
|
|
91
|
+
/** Plus grand numéro de port TCP (RFC 793) : au-delà, `listen` lève un `RangeError`. */
|
|
92
|
+
const MAX_PORT = 65535;
|
|
109
93
|
/** Prochain candidat : incrémente, en sautant ce que les autres serveurs veulent. */
|
|
110
94
|
function nextCandidate(from, reserved) {
|
|
111
95
|
let port = from + 1;
|
|
@@ -176,10 +160,13 @@ async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
|
176
160
|
address: server.address(),
|
|
177
161
|
shiftedFrom: candidate === plan.desired ? null : plan.desired
|
|
178
162
|
};
|
|
179
|
-
|
|
163
|
+
const code = failure.code;
|
|
164
|
+
if (!conflict && !isPortUnavailable(typeof code === "string" ? code : void 0, candidate)) throw failure;
|
|
180
165
|
if (plan.desired === 0 || used >= plan.attempts) throw failure;
|
|
181
166
|
used += 1;
|
|
182
|
-
|
|
167
|
+
const next = nextCandidate(candidate, plan.reserved);
|
|
168
|
+
if (next > MAX_PORT) throw failure;
|
|
169
|
+
candidate = next;
|
|
183
170
|
}
|
|
184
171
|
}
|
|
185
172
|
//#endregion
|
|
@@ -57,8 +57,9 @@ var Session = class {
|
|
|
57
57
|
constructor(name, options, manager) {
|
|
58
58
|
this.options = extend({}, defaultSessionOptions, options);
|
|
59
59
|
this.manager = manager;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const storage = this.manager.storage;
|
|
61
|
+
this.storage = storage;
|
|
62
|
+
if (!storage) this.status = "disabled";
|
|
62
63
|
this.setName(name);
|
|
63
64
|
this.strategy = this.manager.sessionStrategy;
|
|
64
65
|
}
|
|
@@ -195,7 +196,7 @@ var Session = class {
|
|
|
195
196
|
return true;
|
|
196
197
|
}
|
|
197
198
|
setCookieSession(leftTime, options = {}) {
|
|
198
|
-
if (this.context
|
|
199
|
+
if (this.context?.response) {
|
|
199
200
|
const settings = extend({}, this.options.cookie, options);
|
|
200
201
|
if (leftTime) settings.maxAge = leftTime;
|
|
201
202
|
const cookie = new Cookie(this.context.getSessionCookieName(), this.id, settings);
|
|
@@ -207,7 +208,7 @@ var Session = class {
|
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
209
210
|
deleteCookieSession() {
|
|
210
|
-
if (this.context
|
|
211
|
+
if (this.context?.response) {
|
|
211
212
|
let cookie = this.cookieSession;
|
|
212
213
|
if (cookie) cookie.expires = /* @__PURE__ */ new Date(0);
|
|
213
214
|
else cookie = new Cookie(this.context.getSessionCookieName(), "", { expires: /* @__PURE__ */ new Date(0) });
|
|
@@ -219,11 +220,17 @@ var Session = class {
|
|
|
219
220
|
return this.cookieSession;
|
|
220
221
|
}
|
|
221
222
|
isValidSession(_data, context) {
|
|
222
|
-
if (this.options.refererCheck)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
if (this.options.refererCheck) {
|
|
224
|
+
let sameHost = false;
|
|
225
|
+
try {
|
|
226
|
+
sameHost = this.checkSecureReferer(context);
|
|
227
|
+
} catch {
|
|
228
|
+
sameHost = false;
|
|
229
|
+
}
|
|
230
|
+
if (!sameHost) {
|
|
231
|
+
this.log(`SESSION REFERER MISMATCH ==> ${this.name} : ${this.id}`, "WARNING");
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
227
234
|
}
|
|
228
235
|
const now = Date.now();
|
|
229
236
|
if (this.options.absoluteTimeoutS && this.created) {
|
|
@@ -347,7 +354,8 @@ var Session = class {
|
|
|
347
354
|
user: user ?? this.user ?? ""
|
|
348
355
|
};
|
|
349
356
|
}
|
|
350
|
-
deSerialize(
|
|
357
|
+
deSerialize(serialized) {
|
|
358
|
+
const data = serialized;
|
|
351
359
|
if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
352
360
|
if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
353
361
|
if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
1
2
|
//#region nodefony/src/session/sessionStoreFailure.ts
|
|
2
3
|
/**
|
|
3
4
|
* Reconnaît une table de session absente, quel que soit le dialecte.
|
|
@@ -18,7 +19,7 @@ function looksLikeMissingTable(text) {
|
|
|
18
19
|
* @returns le statut, le corps à servir, et la ligne de journal.
|
|
19
20
|
*/
|
|
20
21
|
function describeSessionStoreFailure(error) {
|
|
21
|
-
const cause = error
|
|
22
|
+
const cause = error === null || error === void 0 ? "cause inconnue" : thrownMessage(error);
|
|
22
23
|
return {
|
|
23
24
|
statusCode: 500,
|
|
24
25
|
body: "Internal Server Error — session store unavailable",
|
|
@@ -82,14 +82,29 @@ var RevocationGuardStorage = class {
|
|
|
82
82
|
constructor(inner) {
|
|
83
83
|
this.inner = inner;
|
|
84
84
|
this.sortableFields = inner.sortableFields;
|
|
85
|
-
if (typeof inner.listAll === "function")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (typeof inner.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
85
|
+
if (typeof inner.listAll === "function") {
|
|
86
|
+
const src = inner;
|
|
87
|
+
this.listAll = (filter) => src.listAll(filter);
|
|
88
|
+
}
|
|
89
|
+
if (typeof inner.listPage === "function") {
|
|
90
|
+
const src = inner;
|
|
91
|
+
this.listPage = (query) => src.listPage(query);
|
|
92
|
+
}
|
|
93
|
+
if (typeof inner.countSessions === "function") {
|
|
94
|
+
const src = inner;
|
|
95
|
+
this.countSessions = (query) => src.countSessions(query);
|
|
96
|
+
}
|
|
97
|
+
if (typeof inner.countDistinctUsers === "function") {
|
|
98
|
+
const src = inner;
|
|
99
|
+
this.countDistinctUsers = (query) => src.countDistinctUsers(query);
|
|
100
|
+
}
|
|
101
|
+
if (typeof inner.touch === "function") {
|
|
102
|
+
const src = inner;
|
|
103
|
+
this.touch = (id, idleSeconds) => {
|
|
104
|
+
if (this.#isRevoked(id)) return Promise.resolve();
|
|
105
|
+
return src.touch(id, idleSeconds);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
93
108
|
}
|
|
94
109
|
read(id) {
|
|
95
110
|
return this.inner.read(id);
|
|
@@ -117,7 +132,7 @@ var RevocationGuardStorage = class {
|
|
|
117
132
|
#revoke(id) {
|
|
118
133
|
const now = Date.now();
|
|
119
134
|
this.#purgeExpired(now);
|
|
120
|
-
|
|
135
|
+
this.#tombstones ??= /* @__PURE__ */ new Map();
|
|
121
136
|
this.#tombstones.set(id, now + TOMBSTONE_TTL_MS);
|
|
122
137
|
}
|
|
123
138
|
#isRevoked(id) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Kernel, Module } from "nodefony";
|
|
2
|
+
import { type IHttpOverlay } from "./nodefony/config/config.js";
|
|
2
3
|
import type { IHttpConfig, IHttpConfigInput } from "./nodefony/interfaces/IHttpConfig.js";
|
|
3
4
|
import { Profiler } from "./nodefony/src/profiler/Profiler.js";
|
|
4
5
|
import HttpKernel from "./nodefony/service/http-kernel.js";
|
|
@@ -22,8 +23,21 @@ declare module "nodefony" {
|
|
|
22
23
|
interface NodefonyModuleConfig {
|
|
23
24
|
"@nodefony/http": IHttpConfigInput;
|
|
24
25
|
}
|
|
26
|
+
interface NodefonyModuleOverlay {
|
|
27
|
+
"@nodefony/http": IHttpOverlay;
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
declare class Http extends Module<IHttpConfig> {
|
|
31
|
+
overlaySchema: import("zod").ZodObject<{
|
|
32
|
+
maxBodySize: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
33
|
+
upload: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
34
|
+
maxFileSize: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
35
|
+
maxTotalFileSize: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
36
|
+
maxFiles: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
37
|
+
maxFields: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
38
|
+
maxFieldsSize: import("zod").ZodOptional<import("zod").ZodType<number, number, import("zod/v4/core").$ZodTypeInternals<number, number>>>;
|
|
39
|
+
}, import("zod/v4/core").$strict>>;
|
|
40
|
+
}, import("zod/v4/core").$strict>;
|
|
27
41
|
constructor(kernel: Kernel);
|
|
28
42
|
/**
|
|
29
43
|
* Expose le JSON Schema de la config http (dérivé du schéma Zod, flags `meta()`
|
|
@@ -75,6 +89,8 @@ export { SESSION_SORTABLE_FIELDS, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_S
|
|
|
75
89
|
export type { IHttpRequest, IHttp2Request, IWsRequest, HTTPMethodType, } from "./nodefony/interfaces/IRequest.js";
|
|
76
90
|
export type { IHttpResponse, IWebsocketResponse, } from "./nodefony/interfaces/IResponse.js";
|
|
77
91
|
export type { IHttpKernel } from "./nodefony/interfaces/IHttpKernel.js";
|
|
92
|
+
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./nodefony/interfaces/IRouting.js";
|
|
93
|
+
export type { ISecurityZone, IFirewallGate, } from "./nodefony/interfaces/ISecurity.js";
|
|
78
94
|
export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, } from "./nodefony/interfaces/IUpload.js";
|
|
79
95
|
export type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult, } from "./nodefony/interfaces/IErrorRenderer.js";
|
|
80
96
|
export type { IRequestLogger, IRequestLogEntry, } from "./nodefony/interfaces/IRequestLogger.js";
|
|
@@ -189,6 +189,28 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
189
189
|
export type IHttpConfig = z.infer<typeof httpConfigSchema>;
|
|
190
190
|
/** Type d'entrée (toutes sections omissibles — défauts du schéma). */
|
|
191
191
|
export type IHttpConfigInput = z.input<typeof httpConfigSchema>;
|
|
192
|
+
/**
|
|
193
|
+
* LISTE BLANCHE des clés qu'une requête peut surcharger pour elle seule
|
|
194
|
+
* (`overlayConfig("@nodefony/http", …)`, #494) : les quotas de corps et
|
|
195
|
+
* d'envoi, lus au MOMENT de lire le corps (`Request.readBodyLimits`) — un
|
|
196
|
+
* calque posé avant la lecture du corps (écouteur `onRequestScope`, organisation
|
|
197
|
+
* reconnue à son nom d'hôte) les voit. Tout le reste est refusé : en-têtes de
|
|
198
|
+
* sécurité, pare-feu, `trustProxy`, sessions, limites de débit, certificats —
|
|
199
|
+
* et `queryString`, lu au constructeur de la requête, avant tout calque.
|
|
200
|
+
* Chaque champ réutilise celui du schéma (mêmes bornes) sans son défaut.
|
|
201
|
+
*/
|
|
202
|
+
export declare const httpOverlaySchema: z.ZodObject<{
|
|
203
|
+
maxBodySize: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
204
|
+
upload: z.ZodOptional<z.ZodObject<{
|
|
205
|
+
maxFileSize: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
206
|
+
maxTotalFileSize: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
207
|
+
maxFiles: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
208
|
+
maxFields: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
209
|
+
maxFieldsSize: z.ZodOptional<z.ZodType<number, number, z.core.$ZodTypeInternals<number, number>>>;
|
|
210
|
+
}, z.core.$strict>>;
|
|
211
|
+
}, z.core.$strict>;
|
|
212
|
+
/** Calque accepté par `overlayConfig("@nodefony/http", …)`. */
|
|
213
|
+
export type IHttpOverlay = z.input<typeof httpOverlaySchema>;
|
|
192
214
|
/**
|
|
193
215
|
* Défauts du module, matérialisés depuis le schéma (source unique). Toujours
|
|
194
216
|
* valides par construction ; passés au `super(..., config)` du Module class.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { URL } from "node:url";
|
|
2
|
+
import type { MaybePromise } from "nodefony";
|
|
2
3
|
import type { ICookie } from "./ICookie.js";
|
|
3
4
|
import type { ISession } from "./ISession.js";
|
|
4
5
|
import type { HTTPMethodType } from "./IRequest.js";
|
|
@@ -10,8 +11,8 @@ export type PhaseName = "parse" | "resolve" | "firewall" | "initialize" | "actio
|
|
|
10
11
|
export interface PhaseTiming {
|
|
11
12
|
name: PhaseName;
|
|
12
13
|
startMs: number;
|
|
13
|
-
endMs?: number;
|
|
14
|
-
durationMs?: number;
|
|
14
|
+
endMs?: number | undefined;
|
|
15
|
+
durationMs?: number | undefined;
|
|
15
16
|
}
|
|
16
17
|
/** Issue de la traversée d'une zone firewall. */
|
|
17
18
|
export type SecurityOutcome =
|
|
@@ -110,9 +111,25 @@ export interface IContext {
|
|
|
110
111
|
export interface IHttpContext extends IContext {
|
|
111
112
|
proxy: object | null;
|
|
112
113
|
isRedirect: boolean;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Sert la requête — synchrone quand l'action et son rendu le sont.
|
|
116
|
+
*
|
|
117
|
+
* @returns le résultat du rendu, ou sa promesse dès qu'une étape attend.
|
|
118
|
+
*/
|
|
119
|
+
handle(): MaybePromise<object>;
|
|
120
|
+
/**
|
|
121
|
+
* Sérialise puis envoie — synchrone quand l'envoi l'est.
|
|
122
|
+
*
|
|
123
|
+
* @returns la réponse, ou sa promesse dès qu'une étape attend.
|
|
124
|
+
*/
|
|
125
|
+
render(chunk: unknown, encoding?: BufferEncoding, status?: string | number, headers?: Record<string, string | number>): MaybePromise<object>;
|
|
126
|
+
/**
|
|
127
|
+
* Envoie la réponse — synchrone sans session à sauver, sans hook `onSend`,
|
|
128
|
+
* pour une réponse unique.
|
|
129
|
+
*
|
|
130
|
+
* @returns la réponse, ou sa promesse dès qu'une étape attend.
|
|
131
|
+
*/
|
|
132
|
+
send(chunk?: unknown, encoding?: BufferEncoding): MaybePromise<object>;
|
|
116
133
|
end(): Promise<object>;
|
|
117
134
|
redirect(url: string, status?: number | string, headers?: Record<string, string | number>): unknown;
|
|
118
135
|
getHostName(): string | undefined;
|
|
@@ -121,8 +138,8 @@ export interface IHttpContext extends IContext {
|
|
|
121
138
|
getUserAgent(): string | undefined;
|
|
122
139
|
}
|
|
123
140
|
export interface IWebsocketContext extends IContext {
|
|
124
|
-
acceptedProtocol?: string;
|
|
125
|
-
connection: unknown
|
|
141
|
+
acceptedProtocol?: string | undefined;
|
|
142
|
+
connection: unknown;
|
|
126
143
|
rejected: boolean;
|
|
127
144
|
port: number | string;
|
|
128
145
|
wsUrl: URL | null;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export type SameSiteType = "Strict" | "Lax" | "None";
|
|
2
2
|
export type PriorityType = "High" | "Medium" | "Low" | undefined;
|
|
3
3
|
export interface ICookieOptions {
|
|
4
|
-
maxAge?: number;
|
|
5
|
-
path?: string;
|
|
6
|
-
domain?: string;
|
|
7
|
-
secure?: boolean;
|
|
8
|
-
expires?: Date | string | number;
|
|
9
|
-
sameSite?: SameSiteType;
|
|
10
|
-
httpOnly?: boolean;
|
|
11
|
-
signed?: boolean;
|
|
12
|
-
secret?: string;
|
|
4
|
+
maxAge?: number | undefined;
|
|
5
|
+
path?: string | undefined;
|
|
6
|
+
domain?: string | undefined;
|
|
7
|
+
secure?: boolean | undefined;
|
|
8
|
+
expires?: Date | string | number | undefined;
|
|
9
|
+
sameSite?: SameSiteType | undefined;
|
|
10
|
+
httpOnly?: boolean | undefined;
|
|
11
|
+
signed?: boolean | undefined;
|
|
12
|
+
secret?: string | undefined;
|
|
13
13
|
priority?: PriorityType;
|
|
14
14
|
}
|
|
15
15
|
export interface IWsCookie {
|
|
@@ -26,16 +26,16 @@ export interface ICookie {
|
|
|
26
26
|
name: string;
|
|
27
27
|
value: unknown;
|
|
28
28
|
options: ICookieOptions;
|
|
29
|
-
signed?: boolean;
|
|
30
|
-
originalMaxAge?: number;
|
|
31
|
-
expires?: Date;
|
|
32
|
-
maxAge?: number;
|
|
33
|
-
path?: string;
|
|
29
|
+
signed?: boolean | undefined;
|
|
30
|
+
originalMaxAge?: number | undefined;
|
|
31
|
+
expires?: Date | undefined;
|
|
32
|
+
maxAge?: number | undefined;
|
|
33
|
+
path?: string | undefined;
|
|
34
34
|
domain?: string | undefined;
|
|
35
|
-
httpOnly?: boolean;
|
|
36
|
-
secure?: boolean;
|
|
37
|
-
sameSite?: SameSiteType;
|
|
38
|
-
priority?: string;
|
|
35
|
+
httpOnly?: boolean | undefined;
|
|
36
|
+
secure?: boolean | undefined;
|
|
37
|
+
sameSite?: SameSiteType | undefined;
|
|
38
|
+
priority?: string | undefined;
|
|
39
39
|
setValue(value: unknown): unknown;
|
|
40
40
|
toString(): string;
|
|
41
41
|
serialize(): string;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import type { IServerKernel, MaybePromise } from "nodefony";
|
|
1
2
|
import type { ServerType } from "./IContext.js";
|
|
2
|
-
export interface IHttpKernel {
|
|
3
|
+
export interface IHttpKernel extends IServerKernel {
|
|
3
4
|
domain: string;
|
|
4
|
-
httpPort?: number;
|
|
5
|
-
httpsPort?: number;
|
|
6
|
-
|
|
5
|
+
httpPort?: number | undefined;
|
|
6
|
+
httpsPort?: number | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Pipeline public d'une requête (appelé par les serveurs HTTP/HTTPS).
|
|
9
|
+
*
|
|
10
|
+
* @returns le contexte servi, ou sa promesse dès qu'une étape attend.
|
|
11
|
+
*/
|
|
12
|
+
handle(request: unknown, response: unknown, type: ServerType): MaybePromise<unknown>;
|
|
7
13
|
handleFrontController(context: object, checkFirewall?: boolean): Promise<unknown>;
|
|
8
14
|
onError(error: unknown, context?: object, extraHeaders?: object): Promise<object>;
|
|
9
15
|
isValidDomain(context: object): boolean;
|
|
@@ -31,7 +31,7 @@ export interface IHttpResponse extends IResponse {
|
|
|
31
31
|
setCookie(cookie: ICookie): void;
|
|
32
32
|
}
|
|
33
33
|
export interface IWebsocketResponse extends IResponse {
|
|
34
|
-
connection: unknown
|
|
34
|
+
connection: unknown;
|
|
35
35
|
webSocketVersion?: number;
|
|
36
36
|
setConnection(connection: unknown): unknown;
|
|
37
37
|
broadcast(data: Buffer | string): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { MaybePromise } from "nodefony";
|
|
2
|
+
import type { ContextType } from "../service/http-kernel.js";
|
|
3
|
+
/**
|
|
4
|
+
* Ce que le pipeline HTTP/WS lit d'une route résolue — et rien de plus.
|
|
5
|
+
*
|
|
6
|
+
* Le routage vit dans `@nodefony/framework`, qui dépend de `@nodefony/http` :
|
|
7
|
+
* `http` ne peut donc pas nommer ses classes sans recréer un cycle. C'est le
|
|
8
|
+
* LECTEUR qui définit le contrat ; `Route` (framework) le satisfait par
|
|
9
|
+
* structure, et le typecheck de `framework` garantit l'alignement.
|
|
10
|
+
*/
|
|
11
|
+
export interface IResolvedRoute {
|
|
12
|
+
/** Nom de la route (journalisation, diagnostic). */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Corps laissé en flux (`@Body({ stream: true })`) : le parse est sauté. */
|
|
15
|
+
readonly bodyStream?: boolean | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ce que le pipeline HTTP/WS attend du résolveur d'une requête, posé sur
|
|
19
|
+
* `context.resolver` par le routeur.
|
|
20
|
+
*
|
|
21
|
+
* Implémenté par `Resolver` (`@nodefony/framework`), qui l'étend de ses propres
|
|
22
|
+
* membres — un code de `framework` qui a besoin de la classe entière la tient
|
|
23
|
+
* de son routeur, pas de ce contrat.
|
|
24
|
+
*/
|
|
25
|
+
export interface IRouteResolver {
|
|
26
|
+
/** Route matchée, `null` tant qu'aucune ne l'est. */
|
|
27
|
+
readonly route: IResolvedRoute | null;
|
|
28
|
+
/** `true` si une route a matché la requête. */
|
|
29
|
+
resolve: boolean;
|
|
30
|
+
/** Erreur levée pendant la résolution, relue par le pipeline. */
|
|
31
|
+
exception?: Error | null | undefined;
|
|
32
|
+
/** Classe du controller résolu — seul son nom est lu (erreurs, journal). */
|
|
33
|
+
readonly controller?: {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
} | null | undefined;
|
|
36
|
+
/** Nom de l'action résolue. */
|
|
37
|
+
actionName?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Rejoue le match d'une route sur un contexte (WS : handshake puis frames).
|
|
40
|
+
*
|
|
41
|
+
* @param route - la route à matcher, lue sur {@link IRouteResolver.route}
|
|
42
|
+
* @param context - le contexte courant
|
|
43
|
+
* @param cleanPath - chemin imposé (pont WS-RPC), sinon l'URL du contexte
|
|
44
|
+
*/
|
|
45
|
+
match(route: IResolvedRoute, context: ContextType, cleanPath?: string): unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Instancie le controller résolu — le pipeline ne fait que le transmettre.
|
|
48
|
+
*
|
|
49
|
+
* @param context - le contexte de la requête
|
|
50
|
+
* @returns l'instance du controller — directement quand elle existe déjà ou
|
|
51
|
+
* se construit sans rien attendre, sinon la promesse de sa création
|
|
52
|
+
*/
|
|
53
|
+
newController(context?: ContextType): MaybePromise<object>;
|
|
54
|
+
/**
|
|
55
|
+
* Exécute l'action résolue, puis rend sa valeur sur le transport.
|
|
56
|
+
*
|
|
57
|
+
* @param data - arguments supplémentaires de l'action
|
|
58
|
+
* @param reload - rejouer depuis l'instance en cache (forward)
|
|
59
|
+
* @returns le résultat du rendu — synchrone quand ni l'action ni le rendu
|
|
60
|
+
* n'attendent, sinon sa promesse
|
|
61
|
+
*/
|
|
62
|
+
callController(data?: unknown[], reload?: boolean): MaybePromise<unknown>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Ce que le pipeline HTTP/WS attend du service `router` — implémenté par
|
|
66
|
+
* `Router` (`@nodefony/framework`).
|
|
67
|
+
*/
|
|
68
|
+
export interface IRequestRouter {
|
|
69
|
+
/**
|
|
70
|
+
* Résout la route d'un contexte.
|
|
71
|
+
*
|
|
72
|
+
* @param context - le contexte HTTP/WS courant
|
|
73
|
+
* @param cleanPathOverride - chemin porté par un message (pont WS-RPC)
|
|
74
|
+
* @param methodOverride - méthode logique exigée en plus du transport WS
|
|
75
|
+
* @returns le résolveur (`resolve === true` si une route a matché)
|
|
76
|
+
*/
|
|
77
|
+
resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): IRouteResolver;
|
|
78
|
+
/**
|
|
79
|
+
* Instance partagée d'un controller `scope: "singleton"`, créée une seule fois.
|
|
80
|
+
*
|
|
81
|
+
* @param ctor - la classe du controller (clé du cache)
|
|
82
|
+
* @param create - fabrique exécutée au premier appel
|
|
83
|
+
* @returns l'instance partagée une fois créée ; la promesse de sa création
|
|
84
|
+
* tant qu'elle est en cours
|
|
85
|
+
*/
|
|
86
|
+
getSingletonController<T extends object>(ctor: new (...args: never[]) => T, create: () => MaybePromise<T>): MaybePromise<T>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ContextType } from "../service/http-kernel.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ce que le pipeline HTTP/WS — et le résolveur de framework — lit de la zone de sécurité posée sur
|
|
4
|
+
* `context.security` — et rien de plus.
|
|
5
|
+
*
|
|
6
|
+
* Le pare-feu vit dans `@nodefony/security`, qui dépend de `@nodefony/http` :
|
|
7
|
+
* `http` ne peut donc pas nommer ses classes sans recréer un cycle. C'est le
|
|
8
|
+
* LECTEUR qui définit le contrat ; `ISecuredArea` (security) l'étend, et le
|
|
9
|
+
* typecheck de `security` garantit l'alignement.
|
|
10
|
+
*/
|
|
11
|
+
export interface ISecurityZone {
|
|
12
|
+
/** Nom de la zone (journalisation, profiler). */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** `true` si la zone exige une authentification. */
|
|
15
|
+
readonly security: boolean;
|
|
16
|
+
/** Zone sans session : aucune session n'est ouverte ni lue. */
|
|
17
|
+
readonly stateless: boolean;
|
|
18
|
+
/** Stratégie des authenticators : le premier qui répond, ou tous. */
|
|
19
|
+
readonly mode: "first" | "all";
|
|
20
|
+
/** Noms des authenticators candidats, dans l'ordre. */
|
|
21
|
+
readonly authenticators: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Rôles exigés par la zone, en OU (un seul suffit) ; `null` = aucune exigence
|
|
24
|
+
* de rôle. Lu par le résolveur de `@nodefony/framework` pour l'autorisation.
|
|
25
|
+
*/
|
|
26
|
+
readonly roles: readonly string[] | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ce que le noyau HTTP appelle du pare-feu, service `firewall` du conteneur.
|
|
30
|
+
*
|
|
31
|
+
* Implémenté par `Firewall` (`@nodefony/security`), dont `IFirewall` étend ce
|
|
32
|
+
* contrat de ses propres membres (CSP, authenticators, description).
|
|
33
|
+
*/
|
|
34
|
+
export interface IFirewallGate {
|
|
35
|
+
/** Match rapide de zone : pose `context.security`, `true` si une zone capture. */
|
|
36
|
+
isSecure(context: ContextType): boolean;
|
|
37
|
+
/** Authentifie la requête d'une zone protégée ; rejette si elle est refusée. */
|
|
38
|
+
handleSecurity(context: ContextType): Promise<ContextType>;
|
|
39
|
+
/** Refuse une requête qui ne présente pas le jeton CSRF attendu. */
|
|
40
|
+
enforceCsrf(context: ContextType): void;
|
|
41
|
+
/** Répond au preflight CORS ; `204` quand la réponse est déjà envoyée. */
|
|
42
|
+
handleCors(context: ContextType): number | undefined;
|
|
43
|
+
/** Pose les en-têtes de sécurité (CSP, HSTS…) de la réponse. */
|
|
44
|
+
applySecurityHeaders(context: ContextType): void;
|
|
45
|
+
}
|
|
@@ -13,7 +13,7 @@ export type MetaBagType = Record<string, unknown>;
|
|
|
13
13
|
* - `readOnly` : la session est lue/reprise mais **jamais persistée** (0 write storage).
|
|
14
14
|
*/
|
|
15
15
|
export interface SessionIntent {
|
|
16
|
-
readOnly?: boolean;
|
|
16
|
+
readOnly?: boolean | undefined;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Données de session **sérialisées** échangées avec un {@link ISessionStorage}
|
|
@@ -25,8 +25,8 @@ export interface ISerializedSession {
|
|
|
25
25
|
metaBag: Record<string, unknown>;
|
|
26
26
|
flashBag: Record<string, unknown>;
|
|
27
27
|
user: string;
|
|
28
|
-
createdAt?: Date;
|
|
29
|
-
updatedAt?: Date;
|
|
28
|
+
createdAt?: Date | undefined;
|
|
29
|
+
updatedAt?: Date | undefined;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Projection **redactée** d'une session pour l'ADMINISTRATION (data plane Studio,
|
|
@@ -100,7 +100,7 @@ export interface ISessionListFilter {
|
|
|
100
100
|
/** Restreint aux sessions d'un utilisateur (pour « déconnecter partout »). */
|
|
101
101
|
user?: string;
|
|
102
102
|
/** Slot multi-tenant — non scopé aujourd'hui (réserve coût-0). */
|
|
103
|
-
tenantId?: string | null;
|
|
103
|
+
tenantId?: string | null | undefined;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Requête d'énumération **paginée** des sessions — le {@link IPageQuery} standard
|
|
@@ -253,10 +253,10 @@ export interface ISession {
|
|
|
253
253
|
cookieSession: ICookie | null | undefined;
|
|
254
254
|
flashBag: FlashBagType;
|
|
255
255
|
strategy: SessionStrategyType;
|
|
256
|
-
created?: Date;
|
|
257
|
-
updated?: Date;
|
|
258
|
-
user?: string;
|
|
259
|
-
lifetime?: number;
|
|
256
|
+
created?: Date | undefined;
|
|
257
|
+
updated?: Date | undefined;
|
|
258
|
+
user?: string | undefined;
|
|
259
|
+
lifetime?: number | undefined;
|
|
260
260
|
storage: ISessionStorage;
|
|
261
261
|
start(context: unknown): Promise<ISession>;
|
|
262
262
|
save(user?: string): Promise<ISession>;
|
|
@@ -28,21 +28,21 @@ export interface IParsedUploadFile {
|
|
|
28
28
|
*/
|
|
29
29
|
export interface IUploadOptions {
|
|
30
30
|
/** Répertoire de dépôt des fichiers temporaires. */
|
|
31
|
-
uploadDir?: string;
|
|
31
|
+
uploadDir?: string | undefined;
|
|
32
32
|
/** Taille max d'UN fichier (octets) — `limits.fileSize` busboy. */
|
|
33
|
-
maxFileSize?: number;
|
|
33
|
+
maxFileSize?: number | undefined;
|
|
34
34
|
/** Taille max CUMULÉE des fichiers d'une requête (octets) — appliquée par Nodefony. */
|
|
35
|
-
maxTotalFileSize?: number;
|
|
35
|
+
maxTotalFileSize?: number | undefined;
|
|
36
36
|
/** Nombre max de fichiers — `limits.files` busboy. */
|
|
37
|
-
maxFiles?: number;
|
|
37
|
+
maxFiles?: number | undefined;
|
|
38
38
|
/** Nombre max de champs texte — `limits.fields` busboy. */
|
|
39
|
-
maxFields?: number;
|
|
39
|
+
maxFields?: number | undefined;
|
|
40
40
|
/** Taille max d'un champ texte (octets) — `limits.fieldSize` busboy. */
|
|
41
|
-
maxFieldsSize?: number;
|
|
41
|
+
maxFieldsSize?: number | undefined;
|
|
42
42
|
/** Hash calculé pendant le stream (défaut `false` = aucun). */
|
|
43
|
-
hashAlgorithm?: false | "sha1" | "md5" | "sha256";
|
|
43
|
+
hashAlgorithm?: false | "sha1" | "md5" | "sha256" | undefined;
|
|
44
44
|
/** Encodage par défaut des parts texte. */
|
|
45
|
-
encoding?: string;
|
|
45
|
+
encoding?: string | undefined;
|
|
46
46
|
}
|
|
47
47
|
export interface IUploadedFile {
|
|
48
48
|
filename: string;
|
|
@@ -60,7 +60,7 @@ export interface IUploadedFile {
|
|
|
60
60
|
getPrettySize(): string;
|
|
61
61
|
}
|
|
62
62
|
export interface IUploadService {
|
|
63
|
-
path?:
|
|
63
|
+
path?: unknown;
|
|
64
64
|
/** Construit un `UploadedFile` en async (stat non bloquant). */
|
|
65
65
|
createUploadFile(file: unknown, name: string): Promise<IUploadedFile>;
|
|
66
66
|
}
|
|
@@ -5,3 +5,5 @@ export type { IRequest, IHttpRequest, IHttp2Request, IWsRequest, HTTPMethodType,
|
|
|
5
5
|
export type { IResponse, IHttpResponse, IWebsocketResponse } from "./IResponse.js";
|
|
6
6
|
export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType, WebSocketStateType, CookiesMap, } from "./IContext.js";
|
|
7
7
|
export type { IHttpKernel } from "./IHttpKernel.js";
|
|
8
|
+
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./IRouting.js";
|
|
9
|
+
export type { ISecurityZone, IFirewallGate } from "./ISecurity.js";
|