@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.
- package/LICENSE +544 -0
- package/README.md +77 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +108 -0
- package/dist/nodefony/command/assetsPublishCommand.js +102 -0
- package/dist/nodefony/command/certificatesCommand.js +47 -0
- package/dist/nodefony/command/networkCommand.js +27 -0
- package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
- package/dist/nodefony/config/config.js +335 -0
- package/dist/nodefony/config/defineModuleConfig.js +93 -0
- package/dist/nodefony/interfaces/IContext.js +1 -0
- package/dist/nodefony/interfaces/ICookie.js +1 -0
- package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
- package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
- package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
- package/dist/nodefony/interfaces/IRequest.js +1 -0
- package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
- package/dist/nodefony/interfaces/IResponse.js +1 -0
- package/dist/nodefony/interfaces/ISession.js +1 -0
- package/dist/nodefony/interfaces/IUpload.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +376 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
- package/dist/nodefony/service/audit-logger.js +159 -0
- package/dist/nodefony/service/certificates.js +545 -0
- package/dist/nodefony/service/error-renderer.js +320 -0
- package/dist/nodefony/service/http-kernel.js +948 -0
- package/dist/nodefony/service/pretty-request-logger.js +72 -0
- package/dist/nodefony/service/request-logger.js +54 -0
- package/dist/nodefony/service/servers/clientError.js +20 -0
- package/dist/nodefony/service/servers/server-http.js +135 -0
- package/dist/nodefony/service/servers/server-https.js +204 -0
- package/dist/nodefony/service/servers/server-static.js +192 -0
- package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
- package/dist/nodefony/service/servers/server-websocket.js +104 -0
- package/dist/nodefony/service/servers/serverShutdown.js +31 -0
- package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
- package/dist/nodefony/service/sessions/sessions-service.js +580 -0
- package/dist/nodefony/service/trace.js +72 -0
- package/dist/nodefony/service/upload/upload-service.js +171 -0
- package/dist/nodefony/src/assets/collectAssets.js +34 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
- package/dist/nodefony/src/context/Context.js +415 -0
- package/dist/nodefony/src/context/domainMatcher.js +88 -0
- package/dist/nodefony/src/context/forwarded.js +185 -0
- package/dist/nodefony/src/context/http/HttpContext.js +309 -0
- package/dist/nodefony/src/context/http/Request.js +543 -0
- package/dist/nodefony/src/context/http/Response.js +368 -0
- package/dist/nodefony/src/context/http/parser.js +188 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
- package/dist/nodefony/src/context/http2/Request.js +29 -0
- package/dist/nodefony/src/context/http2/Response.js +97 -0
- package/dist/nodefony/src/context/metaData.js +47 -0
- package/dist/nodefony/src/context/requestId.js +41 -0
- package/dist/nodefony/src/context/trustProxy.js +167 -0
- package/dist/nodefony/src/context/websocket/Response.js +181 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
- package/dist/nodefony/src/cookies/cookie.js +258 -0
- package/dist/nodefony/src/errors/httpError.js +69 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
- package/dist/nodefony/src/profiler/Profiler.js +139 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
- package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
- package/dist/nodefony/src/servers/portBinder.js +114 -0
- package/dist/nodefony/src/session/session.js +390 -0
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
- package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
- package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
- package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
- package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
- package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
- package/dist/types/nodefony/config/config.d.ts +197 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
- package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
- package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
- package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
- package/dist/types/nodefony/service/certificates.d.ts +246 -0
- package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
- package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
- package/dist/types/nodefony/service/request-logger.d.ts +18 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
- package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
- package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
- package/dist/types/nodefony/service/trace.d.ts +39 -0
- package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
- package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
- package/dist/types/nodefony/src/context/Context.d.ts +195 -0
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
- package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
- package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
- package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
- package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
- package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
- package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
- package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
- package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
- package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
- package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
- package/dist/types/nodefony/src/session/session.d.ts +171 -0
- package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
- package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
- package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
- package/docs/cookies.md +365 -0
- package/docs/index.md +163 -0
- package/docs/observabilite.md +460 -0
- package/docs/rate-limit.md +372 -0
- package/docs/servers.md +935 -0
- package/docs/session.md +768 -0
- package/docs/upload.md +460 -0
- package/package.json +101 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compteur de connexions WebSocket CONCURRENTES par IP — backstop opt-in
|
|
3
|
+
* (F6c, revue 0.6). Distinct du {@link MemoryRateLimitStore} : celui-ci compte un
|
|
4
|
+
* DÉBIT d'ouverture par fenêtre (handshakes/s) ; celui-là un NOMBRE de sockets
|
|
5
|
+
* simultanément ouvertes par IP.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ Portée : PAR PROCESS (1 pod). Un vrai plafond global/IP se fait à l'ingress
|
|
8
|
+
* (nginx `limit_conn`, HAProxy `sc_conn_cur`, annotation k8s) — l'edge voit tout le
|
|
9
|
+
* trafic, rejette avant que l'app paie le fd + le handshake TLS, et couvre TOUS les
|
|
10
|
+
* pods. Ce compteur est une défense en profondeur pour le bare-metal/VPS sans
|
|
11
|
+
* ingress. Cf `wsMaxConnectionsPerIp` (config, opt-in, `null` par défaut).
|
|
12
|
+
*
|
|
13
|
+
* Auto-bornée : la Map ne suit que les IP AYANT des sockets ouvertes (bornée par le
|
|
14
|
+
* nombre de connexions réelles), et se vide au fur et à mesure des fermetures — pas
|
|
15
|
+
* besoin d'un GC ni d'un `maxTracked` (contrairement au store de débit qui retient
|
|
16
|
+
* les IP sur toute la fenêtre). Lazy : Map allouée au 1ᵉʳ acquire.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WsConnectionCounter {
|
|
19
|
+
#private;
|
|
20
|
+
/** @param max - plafond de connexions concurrentes par IP (entier > 0). */
|
|
21
|
+
constructor(max: number);
|
|
22
|
+
/**
|
|
23
|
+
* Tente de réserver un créneau pour `ip`. `true` = sous le plafond (compteur
|
|
24
|
+
* incrémenté, appeler {@link release} à la fermeture) ; `false` = plafond atteint
|
|
25
|
+
* (rien n'est incrémenté, la connexion doit être refusée).
|
|
26
|
+
*/
|
|
27
|
+
tryAcquire(ip: string): boolean;
|
|
28
|
+
/** Libère un créneau de `ip` (à la fermeture de la socket). Idempotent-safe. */
|
|
29
|
+
release(ip: string): void;
|
|
30
|
+
/** Plafond configuré (par IP). */
|
|
31
|
+
get max(): number;
|
|
32
|
+
/** Nombre d'IP actuellement suivies (avec ≥ 1 socket ouverte). */
|
|
33
|
+
get trackedIps(): number;
|
|
34
|
+
/** Total de connexions refusées depuis la construction (observabilité). */
|
|
35
|
+
get rejectedTotal(): number;
|
|
36
|
+
}
|
|
37
|
+
export default WsConnectionCounter;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Le vocabulaire de filtre du registre de rate-limit**, en noms PUBLICS —
|
|
3
|
+
* celui que la console écrit dans l'URL (`?limited=true`).
|
|
4
|
+
*
|
|
5
|
+
* `limited` répond à la seule question qu'on pose à ce registre en exploitation :
|
|
6
|
+
* « qui est au plafond en ce moment ? ». Il est booléen STRICT — `?limited=1`
|
|
7
|
+
* est refusé plutôt que lu comme `false`, ce que faisait la comparaison
|
|
8
|
+
* `limitedRaw === "true"` : sur un tableau de bord d'incident, une liste vide
|
|
9
|
+
* obtenue par erreur de syntaxe se lit « aucun client bloqué ».
|
|
10
|
+
*
|
|
11
|
+
* `q` n'y figure pas : c'est une clé du contrat de page, lue par
|
|
12
|
+
* `parsePageQuery`. Le data plane la recopiait à la main — deuxième lecteur du
|
|
13
|
+
* même paramètre, exactement le motif que ce chantier supprime.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RATE_LIMIT_FILTERS: {
|
|
16
|
+
/** `true` = seulement les clés au plafond, `false` = seulement les autres. */
|
|
17
|
+
readonly limited: "boolean";
|
|
18
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
3
|
+
*
|
|
4
|
+
* Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
|
|
5
|
+
* une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
|
|
6
|
+
* en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
|
|
7
|
+
* un CONTRAT en production.
|
|
8
|
+
*
|
|
9
|
+
* ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
|
|
10
|
+
*
|
|
11
|
+
* La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
|
|
12
|
+
* une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
|
|
13
|
+
* prendre le port — et on échoue quand même, après avoir cru le contraire. Le
|
|
14
|
+
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
15
|
+
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
16
|
+
*
|
|
17
|
+
* ## Pourquoi sauter les ports réservés
|
|
18
|
+
*
|
|
19
|
+
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
20
|
+
* voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
|
|
21
|
+
* AUTRES serveurs convoitent sont donc sautés d'emblée.
|
|
22
|
+
*
|
|
23
|
+
* ## Fail-loud
|
|
24
|
+
*
|
|
25
|
+
* Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
|
|
26
|
+
* là où on l'attend sans le dire est une dégradation silencieuse.
|
|
27
|
+
*/
|
|
28
|
+
import type { AddressInfo } from "node:net";
|
|
29
|
+
/** Serveur écoutable (surface minimale commune `http`/`https`/`http2`). */
|
|
30
|
+
export interface Listenable {
|
|
31
|
+
listen(port: number, host?: string): unknown;
|
|
32
|
+
address(): AddressInfo | string | null;
|
|
33
|
+
once(event: string, listener: (...args: never[]) => void): unknown;
|
|
34
|
+
removeListener(event: string, listener: (...args: never[]) => void): unknown;
|
|
35
|
+
}
|
|
36
|
+
/** Que faire si le port désiré est occupé. */
|
|
37
|
+
export type PortPolicy = "auto" | "strict";
|
|
38
|
+
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
39
|
+
export declare const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
40
|
+
export interface BindPlan {
|
|
41
|
+
/** Port voulu (config). `0` = le noyau choisit (aucun repli nécessaire). */
|
|
42
|
+
desired: number;
|
|
43
|
+
/** Ports convoités par les AUTRES serveurs — jamais volés. */
|
|
44
|
+
reserved: readonly number[];
|
|
45
|
+
/** Essais après le désiré. `0` ⇒ comportement strict. */
|
|
46
|
+
attempts: number;
|
|
47
|
+
}
|
|
48
|
+
export interface BindResult {
|
|
49
|
+
/** Adresse réellement obtenue (le port peut différer du désiré). */
|
|
50
|
+
address: AddressInfo;
|
|
51
|
+
/** Port désiré si l'écoute a dû être décalée, `null` si on l'a obtenu. */
|
|
52
|
+
shiftedFrom: number | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Politique de port effective.
|
|
56
|
+
*
|
|
57
|
+
* Le défaut dépend de l'environnement, et ce n'est pas de la coquetterie :
|
|
58
|
+
* - **production** → `strict`. Le port y est un contrat (service k8s, ingress,
|
|
59
|
+
* sonde de santé). Un bind silencieux ailleurs donnerait un pod déclaré sain
|
|
60
|
+
* que personne n'atteint : une panne invisible, le pire des deux mondes.
|
|
61
|
+
* - **test** → `strict`. Un port occupé veut dire « un serveur est resté debout » ;
|
|
62
|
+
* le banc doit s'arrêter, pas viser à côté (il taperait le serveur du voisin).
|
|
63
|
+
* - **développement** → `auto`. Ici un port pris n'est qu'une nuisance.
|
|
64
|
+
*
|
|
65
|
+
* @param environment - `kernel.environment` (normalisé `development`/`production`/`test`).
|
|
66
|
+
* @param explicit - `servers.portPolicy` s'il est configuré (il gagne toujours).
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolvePortPolicy(environment: string | undefined, explicit?: PortPolicy): PortPolicy;
|
|
69
|
+
/** Forme lue de `kernel.options.servers` (lecture structurelle, pas d'import core). */
|
|
70
|
+
export interface ServersPortConfig {
|
|
71
|
+
http?: {
|
|
72
|
+
port?: number;
|
|
73
|
+
} | false;
|
|
74
|
+
https?: {
|
|
75
|
+
port?: number;
|
|
76
|
+
} | false;
|
|
77
|
+
portPolicy?: PortPolicy;
|
|
78
|
+
portRetryAttempts?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compose le plan de bind d'un serveur depuis la config du kernel — source UNIQUE
|
|
82
|
+
* (les deux serveurs l'appellent ; la politique n'est décidée qu'ici).
|
|
83
|
+
*
|
|
84
|
+
* @param which - le serveur qu'on borne.
|
|
85
|
+
* @param servers - `kernel.options.servers`.
|
|
86
|
+
* @param environment - `kernel.environment` (arbitre le défaut de la politique).
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildBindPlan(which: "http" | "https", servers: ServersPortConfig | undefined, environment: string | undefined): BindPlan;
|
|
89
|
+
/**
|
|
90
|
+
* Écoute sur `plan.desired`, ou sur le prochain port libre si `attempts > 0`.
|
|
91
|
+
*
|
|
92
|
+
* Aucun `error` permanent ne doit être attaché au serveur pendant l'appel : cette
|
|
93
|
+
* fonction pose ses propres écouteurs le temps du bind et les retire toujours (le
|
|
94
|
+
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
95
|
+
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
96
|
+
*
|
|
97
|
+
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
98
|
+
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
99
|
+
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
100
|
+
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
101
|
+
*/
|
|
102
|
+
export declare function bindWithFallback(server: Listenable, host: string, plan: BindPlan): Promise<BindResult>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import Cookie, { CookieOptionsType } from "../cookies/cookie.js";
|
|
2
|
+
import type { ContextType } from "../../service/http-kernel.js";
|
|
3
|
+
import type SessionsService from "../../service/sessions/sessions-service.js";
|
|
4
|
+
import type { ISession, ISessionStorage, ISerializedSession, SessionStatusType, SessionStrategyType, FlashBagType, MetaBagType } from "../../interfaces/ISession.js";
|
|
5
|
+
/**
|
|
6
|
+
* Options d'une session (sous-ensemble de `module.options.session`).
|
|
7
|
+
*
|
|
8
|
+
* Cookie-only : plus de `use_trans_sid`/`use_only_cookies` — un identifiant de
|
|
9
|
+
* session ne voyage JAMAIS dans l'URL (OWASP Session Management). Plus de
|
|
10
|
+
* `encrypt`/`hash_function` — l'identifiant est un secret opaque CSPRNG, pas un
|
|
11
|
+
* hash chiffré « maison ».
|
|
12
|
+
*/
|
|
13
|
+
export type OptionsSessionType = {
|
|
14
|
+
name?: string;
|
|
15
|
+
cookie?: CookieOptionsType;
|
|
16
|
+
/** Strict mode : un identifiant inconnu du storage → nouvelle session (anti-fixation). */
|
|
17
|
+
strictMode?: boolean;
|
|
18
|
+
/** Lie la session à l'hôte (méta `host`) — rejette un changement d'origine. */
|
|
19
|
+
refererCheck?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Idle timeout (NIST/OWASP, secondes) : inactivité MAX depuis la dernière
|
|
22
|
+
* activité (`updatedAt`, rafraîchi par le touch). `0`/absent = pas d'expiration
|
|
23
|
+
* par inactivité. Enforcement serveur ({@link Session.isValidSession} + GC).
|
|
24
|
+
*/
|
|
25
|
+
idleTimeoutS?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Absolute timeout (OWASP, secondes) : durée de vie MAX depuis la création,
|
|
28
|
+
* indépendante de l'activité. Borne la fenêtre d'exploitation d'un identifiant
|
|
29
|
+
* volé même sur session active. `0`/absent = désactivé (seul l'idle s'applique).
|
|
30
|
+
*/
|
|
31
|
+
absoluteTimeoutS?: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Session serveur Nodefony — état persistant lié à une identité, indexé par un
|
|
35
|
+
* identifiant **opaque** (porté par le cookie) dans un {@link ISessionStorage}
|
|
36
|
+
* pluggable (File / Redis / SQL). Modèle BFF : le cookie ne transporte qu'un
|
|
37
|
+
* secret aléatoire, jamais de données ni de JWT.
|
|
38
|
+
*
|
|
39
|
+
* Objet **léger** : trois sacs `{}` (attributs / métas / flash)
|
|
40
|
+
* au lieu d'un `Container` DI par session.
|
|
41
|
+
*
|
|
42
|
+
* Dirty-tracking : toute mutation (`set` / `setFlashBag` / `setMetaBag` /
|
|
43
|
+
* `getFlashBag` qui consomme / …) lève {@link dirty} ; {@link save} n'écrit dans
|
|
44
|
+
* le storage **que** si `dirty`. Une requête qui ne touche pas la session ne
|
|
45
|
+
* déclenche aucune écriture (supprime la contention `write`/requête).
|
|
46
|
+
*/
|
|
47
|
+
declare class Session implements ISession {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
status: SessionStatusType;
|
|
51
|
+
storage: ISessionStorage;
|
|
52
|
+
manager: SessionsService;
|
|
53
|
+
saved: boolean;
|
|
54
|
+
migrated: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Lecture seule (intent `@UseSession({ readOnly })`) : la session est reprise et
|
|
57
|
+
* lue mais **jamais persistée** — {@link save} devient un no-op. Différenciateur
|
|
58
|
+
* perf : une route qui ne fait que LIRE la session (afficher le user…) ne paie
|
|
59
|
+
* aucune écriture storage.
|
|
60
|
+
*/
|
|
61
|
+
readOnly: boolean;
|
|
62
|
+
context?: ContextType;
|
|
63
|
+
created?: Date;
|
|
64
|
+
updated?: Date;
|
|
65
|
+
options: OptionsSessionType;
|
|
66
|
+
cookieSession: Cookie | null;
|
|
67
|
+
lifetime?: number;
|
|
68
|
+
user?: string;
|
|
69
|
+
strategy: SessionStrategyType;
|
|
70
|
+
/** Sac d'attributs applicatifs (clé/valeur) — exposé via {@link getAttributes}. */
|
|
71
|
+
private attributesBag;
|
|
72
|
+
/** Sac de métadonnées techniques (host, ip, ua…) — exposé via {@link getMetas}. */
|
|
73
|
+
private metaBagStore;
|
|
74
|
+
/** Sac de messages flash (consommés à la lecture). Public : lu par les storages. */
|
|
75
|
+
flashBag: FlashBagType;
|
|
76
|
+
/** Drapeau dirty-tracking — lu via le getter {@link dirty}. */
|
|
77
|
+
private mutated;
|
|
78
|
+
constructor(name: string, options: OptionsSessionType, manager: SessionsService);
|
|
79
|
+
/** Vrai si la session a été mutée sans être encore persistée (dirty-tracking). */
|
|
80
|
+
get dirty(): boolean;
|
|
81
|
+
private log;
|
|
82
|
+
/**
|
|
83
|
+
* Démarre (ou reprend) la session. Cookie présent → reprise depuis le storage ;
|
|
84
|
+
* sinon → nouvelle session.
|
|
85
|
+
*
|
|
86
|
+
* @param context - contexte HTTP/HTTP2/WS courant.
|
|
87
|
+
*/
|
|
88
|
+
start(context: ContextType): Promise<this>;
|
|
89
|
+
/**
|
|
90
|
+
* Lit l'identifiant opaque du cookie puis reprend la session, ou en crée une
|
|
91
|
+
* neuve si aucun cookie. Cookie-only : aucun identifiant lu depuis l'URL.
|
|
92
|
+
*/
|
|
93
|
+
private getSession;
|
|
94
|
+
/**
|
|
95
|
+
* Reprend la session `id` depuis le storage. Invalide (→ session neuve) si
|
|
96
|
+
* introuvable en strict mode, ou expirée/illégitime.
|
|
97
|
+
*/
|
|
98
|
+
private resume;
|
|
99
|
+
/**
|
|
100
|
+
* Crée une session neuve : identifiant opaque CSPRNG, cookie, métadonnées.
|
|
101
|
+
* Marquée `dirty` (sauf `saveUninitialized:false`) → persistée + `Set-Cookie`
|
|
102
|
+
* au prochain {@link save}.
|
|
103
|
+
*/
|
|
104
|
+
create(lifetime: number, id?: string, settingsCookie?: CookieOptionsType): this;
|
|
105
|
+
/** Génère un identifiant de session opaque (32 octets CSPRNG → base64url). */
|
|
106
|
+
private generateId;
|
|
107
|
+
/**
|
|
108
|
+
* Régénère l'identifiant (nouveau secret opaque) en conservant l'état courant.
|
|
109
|
+
* Anti session-fixation (OWASP) — appelée à chaque ouverture de session
|
|
110
|
+
* authentifiée par `AuthFlow.#openSession()` (`@nodefony/security`), qui
|
|
111
|
+
* détruit ensuite l'ancienne entrée de storage. Repositionne le cookie et
|
|
112
|
+
* marque la session `dirty`.
|
|
113
|
+
*/
|
|
114
|
+
regenerateId(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Persiste la session **si elle est dirty** (sinon no-op). Réécrit le blob
|
|
117
|
+
* sérialisé dans le storage, repositionne created/updated, lève l'événement
|
|
118
|
+
* `onSaveSession`.
|
|
119
|
+
*
|
|
120
|
+
* @param user - principal authentifié (string) lié au blob ; défaut courant.
|
|
121
|
+
*/
|
|
122
|
+
save(user?: string): Promise<this>;
|
|
123
|
+
/**
|
|
124
|
+
* Détruit la session courante (storage) et en recrée une neuve (nouvel
|
|
125
|
+
* identifiant + cookie). État applicatif réinitialisé.
|
|
126
|
+
*/
|
|
127
|
+
invalidate(lifetime?: number, id?: string, settingsCookie?: CookieOptionsType): Promise<this>;
|
|
128
|
+
/**
|
|
129
|
+
* Détruit la session : vide les sacs, supprime l'entrée storage, et (option)
|
|
130
|
+
* efface le cookie.
|
|
131
|
+
*/
|
|
132
|
+
destroy(cookieDelete?: boolean): Promise<boolean>;
|
|
133
|
+
setCookieSession(leftTime: number, options?: CookieOptionsType): Cookie | null;
|
|
134
|
+
deleteCookieSession(): Cookie | null;
|
|
135
|
+
isValidSession(_data: ISerializedSession, context: ContextType): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Prolonge l'idle timeout de la session active sur l'activité courante, **de
|
|
138
|
+
* façon throttlée** (1 écriture par tranche d'idle, jamais par requête) et
|
|
139
|
+
* **sans réécrire le blob** (write léger `touch` du storage) — l'activité
|
|
140
|
+
* HTTP/WS réelle, **y compris en lecture seule**, empêche l'expiration d'une
|
|
141
|
+
* session utilisée (NIST/OWASP : idle « since the last request »). N'affecte
|
|
142
|
+
* **jamais** l'absolute timeout (borné à la création).
|
|
143
|
+
*
|
|
144
|
+
* No-op si : pas active, pas d'entrée storage à prolonger (`!updated`), idle
|
|
145
|
+
* désactivé, store sans `touch`, ou dernière activité trop récente (throttle =
|
|
146
|
+
* mi-vie de l'idle). Met à jour `updated` localement → le throttle compte
|
|
147
|
+
* depuis ce touch.
|
|
148
|
+
*/
|
|
149
|
+
touchIfNeeded(): Promise<void>;
|
|
150
|
+
checkSecureReferer(context: ContextType): boolean;
|
|
151
|
+
private setMetasSession;
|
|
152
|
+
get(key: string): unknown;
|
|
153
|
+
set(key: string, value: unknown): unknown;
|
|
154
|
+
getAttributes(): Record<string, unknown>;
|
|
155
|
+
getMetaBag(key: string): unknown;
|
|
156
|
+
setMetaBag(key: string, value: unknown): void;
|
|
157
|
+
getMetas(): MetaBagType;
|
|
158
|
+
getFlashBag(key: string): unknown;
|
|
159
|
+
setFlashBag(key: string, value: unknown): unknown;
|
|
160
|
+
flashBags(): FlashBagType;
|
|
161
|
+
clearFlashBag(key: string): void;
|
|
162
|
+
clearFlashBags(): void;
|
|
163
|
+
serialize(user?: string): ISerializedSession;
|
|
164
|
+
deSerialize(data: ISerializedSession): void;
|
|
165
|
+
/** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
|
|
166
|
+
clear(): void;
|
|
167
|
+
getName(): string;
|
|
168
|
+
setName(name: string): void;
|
|
169
|
+
checkStatus(): "restart" | boolean;
|
|
170
|
+
}
|
|
171
|
+
export default Session;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type sessionService from "../../../service/sessions/sessions-service.js";
|
|
3
|
+
import type { ISessionStorage, ISerializedSession, ISessionRecord, ISessionListFilter, ISessionListQuery } from "../../../interfaces/ISession.js";
|
|
4
|
+
/**
|
|
5
|
+
* Store de sessions **en mémoire** (Map process) — implémentation de référence
|
|
6
|
+
* d'{@link ISessionStorage}, pendant `session` des `Memory*Store` de sécurité.
|
|
7
|
+
*
|
|
8
|
+
* **Volatil** : les sessions vivent dans la RAM du process et disparaissent au
|
|
9
|
+
* redémarrage ET ne sont PAS partagées entre pods/workers. Cible : **tests de
|
|
10
|
+
* charge** (mesurer le framework sans le goulot disque/SQL), CI, environnements
|
|
11
|
+
* éphémères. Pour la persistance mono-nœud → `drizzle` (sqlite) ; multi-nœud →
|
|
12
|
+
* `redis`/`drizzle`/`mongoose`.
|
|
13
|
+
*
|
|
14
|
+
* Bornes NIST/OWASP portées par des horodatages internes : `updatedAt` = dernière
|
|
15
|
+
* activité (idle, rafraîchi par {@link touch}), `createdAt` = création (absolute,
|
|
16
|
+
* JAMAIS prolongé). Même sémantique que les stores SQL — l'idle glissant et
|
|
17
|
+
* l'absolute s'appliquent identiquement.
|
|
18
|
+
*/
|
|
19
|
+
declare class MemorySessionStorage implements ISessionStorage {
|
|
20
|
+
#private;
|
|
21
|
+
manager: sessionService;
|
|
22
|
+
idleTimeoutS: number;
|
|
23
|
+
absoluteTimeoutS: number;
|
|
24
|
+
/**
|
|
25
|
+
* Trie sur tout le vocabulaire public : les données sont déjà en RAM, aucun
|
|
26
|
+
* champ n'est plus coûteux qu'un autre. Aucune traduction — les clés internes
|
|
27
|
+
* portent déjà ces noms (`id` étant la clé de la Map).
|
|
28
|
+
*/
|
|
29
|
+
readonly sortableFields: readonly ["updatedAt", "createdAt", "user", "id"];
|
|
30
|
+
constructor(manager: sessionService);
|
|
31
|
+
/** Lecture par id — copie superficielle (le consommateur ne mute pas le store). */
|
|
32
|
+
read(id: string): Promise<ISerializedSession>;
|
|
33
|
+
start(id: string): Promise<ISerializedSession>;
|
|
34
|
+
/**
|
|
35
|
+
* Écrit (upsert) le blob. `createdAt` est FIXÉ à la création et préservé aux
|
|
36
|
+
* updates (borne absolute) ; `updatedAt` est posé à chaque écriture (borne idle).
|
|
37
|
+
*/
|
|
38
|
+
write(id: string, data: ISerializedSession): Promise<ISerializedSession>;
|
|
39
|
+
/** Compte des sessions présentes (+ passe GC comme les autres stores à l'open). */
|
|
40
|
+
open(): Promise<number>;
|
|
41
|
+
close(): boolean;
|
|
42
|
+
destroy(id: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Prolonge l'idle (timeout glissant) : rafraîchit `updatedAt` SANS toucher
|
|
45
|
+
* `createdAt` (borne absolute intacte). Session absente (purgée) → no-op.
|
|
46
|
+
*/
|
|
47
|
+
touch(id: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Purge idle (inactivité depuis `updatedAt`) ET absolute (âge depuis `createdAt`,
|
|
50
|
+
* jamais prolongé). Une borne à 0 = désactivée. Déterministe (synchrone).
|
|
51
|
+
*/
|
|
52
|
+
gc(idleSeconds?: number, absoluteSeconds?: number): Promise<void>;
|
|
53
|
+
/** Énumération admin — filtre `user` appliqué en mémoire. */
|
|
54
|
+
listAll(filter?: ISessionListFilter): Promise<ISessionRecord[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Pagination **offset** avec `total` exact, ordre `updatedAt` DESC (id ASC en
|
|
57
|
+
* départage). Les données étant déjà en RAM par conception, le coût par requête
|
|
58
|
+
* est celui du tri des **références** filtrées — aucune copie de blob n'est
|
|
59
|
+
* faite hors de la page rendue.
|
|
60
|
+
*
|
|
61
|
+
* **Redaction par construction** (garantie du contrat, pas une optimisation) :
|
|
62
|
+
* `Attributes`/`flashBag` sortent VIDES, comme chez les stores SQL/NoSQL qui ne
|
|
63
|
+
* les SELECTent pas. Ici c'est gratuit — on ne recopie simplement pas ces deux
|
|
64
|
+
* bags — et ça aligne le store mémoire sur la même garantie : un record
|
|
65
|
+
* d'énumération admin ne porte jamais de donnée métier.
|
|
66
|
+
*/
|
|
67
|
+
listPage(query: ISessionListQuery): Promise<IPage<ISessionRecord>>;
|
|
68
|
+
/** `COUNT` filtré — parcourt sans allouer (aucun record matérialisé). */
|
|
69
|
+
countSessions(query?: Partial<ISessionListQuery>): Promise<number>;
|
|
70
|
+
/**
|
|
71
|
+
* `COUNT(DISTINCT user)` en mémoire. Le `Set` est alloué à l'appel et relâché
|
|
72
|
+
* aussitôt : c'est un chemin d'administration, appelé à l'ouverture d'un
|
|
73
|
+
* écran, jamais dans le pipeline de requête.
|
|
74
|
+
*/
|
|
75
|
+
countDistinctUsers(query?: Partial<ISessionListQuery>): Promise<number>;
|
|
76
|
+
}
|
|
77
|
+
export default MemorySessionStorage;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type { ISessionStorage, ISerializedSession, ISessionRecord, ISessionListFilter, ISessionListQuery } from "../../../interfaces/ISession.js";
|
|
3
|
+
/**
|
|
4
|
+
* Garde-fou de révocation **décoré au-dessus** de n'importe quel
|
|
5
|
+
* {@link ISessionStorage} (file, drizzle, redis, mongo…). Pose une « pierre
|
|
6
|
+
* tombale » sur tout id détruit et REFUSE un `write` ultérieur de ce même id
|
|
7
|
+
* pendant {@link TOMBSTONE_TTL_MS}.
|
|
8
|
+
*
|
|
9
|
+
* **Pourquoi ici, pas dans chaque store** : la résurrection est une propriété du
|
|
10
|
+
* CYCLE DE VIE de session — révoquer une session puis l'autosave de fin de
|
|
11
|
+
* requête (la requête la portait encore en mémoire, `dirty`) la ré-écrit — et
|
|
12
|
+
* NON d'un backend particulier. Un seul garde-fou décoré sur le storage actif
|
|
13
|
+
* couvre donc TOUS les backends, présents et futurs, **sans en modifier aucun**.
|
|
14
|
+
*
|
|
15
|
+
* Découvert en live le 2026-06-21 (« révoquer une session ne déconnecte pas ») ;
|
|
16
|
+
* le store réel était `drizzle`, pas `files` — d'où la décoration générique.
|
|
17
|
+
*
|
|
18
|
+
* Couvre deux scénarios :
|
|
19
|
+
* - **self** : l'admin révoque SA PROPRE session ; l'autosave de la requête de
|
|
20
|
+
* révocation tente de la réécrire → refusé.
|
|
21
|
+
* - **race** : un autre client du user révoqué a une requête en vol dont
|
|
22
|
+
* l'autosave arrive après la révocation → refusé.
|
|
23
|
+
*
|
|
24
|
+
* **Perf** : décorateur **singleton** (1 par service). Délégation directe sur le
|
|
25
|
+
* hot-path lecture (`read`/`start`/`listAll`) ; `write` ne paie qu'un test
|
|
26
|
+
* `=== null` tant qu'aucune session n'a été révoquée (Map **lazy**, jamais de
|
|
27
|
+
* `Date.now()` dans ce cas).
|
|
28
|
+
*/
|
|
29
|
+
declare class RevocationGuardStorage implements ISessionStorage {
|
|
30
|
+
#private;
|
|
31
|
+
/** Storage réel décoré — exposé pour l'introspection admin (nom du driver). */
|
|
32
|
+
readonly inner: ISessionStorage;
|
|
33
|
+
/**
|
|
34
|
+
* Énumération admin — (ré)assignée dans le constructeur **uniquement** si le
|
|
35
|
+
* backend décoré la supporte, pour que `SessionsService.supportsEnumeration`
|
|
36
|
+
* (`typeof storage.listAll === "function"`) reflète la vraie capacité du store.
|
|
37
|
+
*/
|
|
38
|
+
listAll?: (filter?: ISessionListFilter) => Promise<ISessionRecord[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Touch (prolongation d'idle) — (ré)assignée seulement si le backend décoré la
|
|
41
|
+
* supporte, pour que `Session.touchIfNeeded` (`typeof storage.touch`) reflète la
|
|
42
|
+
* vraie capacité. **Respecte la pierre tombale** : ne prolonge JAMAIS une session
|
|
43
|
+
* révoquée (cohérent avec `write` — une révocation ne doit pas être contournée
|
|
44
|
+
* par un touch tardif d'une requête en vol).
|
|
45
|
+
*/
|
|
46
|
+
touch?: (id: string, idleSeconds?: number) => Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Énumération admin **paginée** — (ré)assignée seulement si le backend décoré
|
|
49
|
+
* la supporte, même raison que {@link listAll} : `supportsEnumeration` teste la
|
|
50
|
+
* présence de la méthode, elle doit donc refléter la vraie capacité du store
|
|
51
|
+
* décoré et non celle du décorateur.
|
|
52
|
+
*/
|
|
53
|
+
listPage?: (query: ISessionListQuery) => Promise<IPage<ISessionRecord>>;
|
|
54
|
+
/** `COUNT` filtré — (ré)assigné seulement si le backend décoré le supporte. */
|
|
55
|
+
countSessions?: (query?: Partial<ISessionListQuery>) => Promise<number>;
|
|
56
|
+
/**
|
|
57
|
+
* `COUNT(DISTINCT user)` — (ré)assigné seulement si le backend décoré le
|
|
58
|
+
* supporte, même raison que {@link countSessions} : une capacité perdue dans
|
|
59
|
+
* le décorateur ferait afficher « inconnu » là où la base sait répondre.
|
|
60
|
+
*/
|
|
61
|
+
countDistinctUsers?: (query?: Partial<ISessionListQuery>) => Promise<number>;
|
|
62
|
+
/**
|
|
63
|
+
* Capacité de tri du backend décoré, relayée telle quelle.
|
|
64
|
+
*
|
|
65
|
+
* Une capacité qui se PERD dans un décorateur est pire qu'une capacité
|
|
66
|
+
* absente : elle est déclarée par le store réel, invisible au-dessus, et le
|
|
67
|
+
* data plane refuse alors (400) un tri que la base sait parfaitement faire.
|
|
68
|
+
* Comme ce décorateur est posé en production dès qu'une révocation est
|
|
69
|
+
* possible, l'oubli aurait désactivé le tri **partout**.
|
|
70
|
+
*/
|
|
71
|
+
readonly sortableFields?: readonly string[];
|
|
72
|
+
constructor(inner: ISessionStorage);
|
|
73
|
+
read(id: string): Promise<ISerializedSession>;
|
|
74
|
+
start(id: string): Promise<ISerializedSession>;
|
|
75
|
+
open(): Promise<number>;
|
|
76
|
+
close(): boolean;
|
|
77
|
+
gc(idleSeconds?: number, absoluteSeconds?: number): Promise<void>;
|
|
78
|
+
destroy(id: string): Promise<boolean>;
|
|
79
|
+
write(id: string, data: ISerializedSession): Promise<ISerializedSession>;
|
|
80
|
+
}
|
|
81
|
+
export default RevocationGuardStorage;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { FacetCount, FacetCounts } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de filtre des sessions**, en noms PUBLICS — celui que la
|
|
4
|
+
* console d'administration écrit dans l'URL (`?user=alice`).
|
|
5
|
+
*
|
|
6
|
+
* Frère de `SESSION_SORTABLE_FIELDS` (`sessionSort.ts`), posé au même endroit
|
|
7
|
+
* pour la même raison : le vocabulaire appartient au module propriétaire du
|
|
8
|
+
* contrat, la mécanique de lecture au cœur (`parseFilters`).
|
|
9
|
+
*
|
|
10
|
+
* `user` est un filtre **portable par construction** (égalité stricte) : il
|
|
11
|
+
* s'exprime dans tous les backends — `WHERE` indexé en SQL et Mongo, prédicat en
|
|
12
|
+
* mémoire, filtre de batch en Redis — donc aucun store n'a besoin de matérialiser
|
|
13
|
+
* la collection pour l'honorer. C'est la propriété qui justifie qu'il soit au
|
|
14
|
+
* contrat plutôt que dans une couche de filtrage applicative.
|
|
15
|
+
*
|
|
16
|
+
* `tenantId` n'y figure pas parce qu'il appartient au **contrat de page**
|
|
17
|
+
* (`PAGE_QUERY_KEYS`, réserve multi-tenant) : le déclarer ici en ferait un
|
|
18
|
+
* second lecteur du même paramètre.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SESSION_FILTERS: {
|
|
21
|
+
/** Sessions d'un utilisateur donné (égalité stricte sur l'identifiant). */
|
|
22
|
+
readonly user: "string";
|
|
23
|
+
/**
|
|
24
|
+
* `true` = sessions rattachées à un utilisateur, `false` = anonymes, absent =
|
|
25
|
+
* les deux. Le contrat le définit comme « `user` non vide » — tous les stores
|
|
26
|
+
* l'honorent déjà (c'est ainsi que `sessions/stats` compte ses facettes).
|
|
27
|
+
*
|
|
28
|
+
* Il n'était pas exposé à l'URL, et la console ne pouvait donc pas MONTRER la
|
|
29
|
+
* population qu'elle affichait en carte : cliquer « Anonymes » demandait un
|
|
30
|
+
* paramètre que la liste refusait. Une facette n'existe que si le contrat de
|
|
31
|
+
* liste sait la filtrer — celle-ci le savait partout sauf sur le fil.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ Redondant avec `user` **par construction** (même donnée). Les combiner
|
|
34
|
+
* de façon contradictoire (`?user=alice&authenticated=false`) rend l'ENSEMBLE
|
|
35
|
+
* VIDE, jamais la page de l'un des deux — garde posée dans le banc de contrat
|
|
36
|
+
* partagé, donc rejouée sur les six backends.
|
|
37
|
+
*/
|
|
38
|
+
readonly authenticated: "boolean";
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* **Les facettes des sessions** — les questions fermées que la console pose à la
|
|
42
|
+
* collection ENTIÈRE, pour ses cartes de tête.
|
|
43
|
+
*
|
|
44
|
+
* Distinct de `SESSION_FILTERS`, qui dit ce qu'un client a le droit d'écrire
|
|
45
|
+
* dans l'URL : une facette n'est pas saisie, elle est posée par le serveur. Les
|
|
46
|
+
* deux se recoupent par construction — une facette n'existe que si le contrat de
|
|
47
|
+
* liste sait déjà la filtrer, sinon elle serait un compteur approximatif de plus.
|
|
48
|
+
*
|
|
49
|
+
* `authenticated` suffit à couvrir les deux populations : le contrat le définit
|
|
50
|
+
* comme « `user` non vide », donc `false` rend exactement les sessions anonymes.
|
|
51
|
+
* Aucune facette n'est déduite d'une soustraction (cf `countFacets`).
|
|
52
|
+
*
|
|
53
|
+
* Le décompte d'utilisateurs **distincts** n'est PAS ici : ce n'est pas un
|
|
54
|
+
* `COUNT` filtré mais une agrégation, qu'un store en curseur ne peut pas rendre.
|
|
55
|
+
* Il vit en capacité déclarée du backend (`ISessionStorage.countDistinctUsers`).
|
|
56
|
+
*/
|
|
57
|
+
export declare const SESSION_FACETS: {
|
|
58
|
+
/** Toutes les sessions persistées, sans filtre. */
|
|
59
|
+
readonly total: {};
|
|
60
|
+
/** Sessions rattachées à un utilisateur authentifié. */
|
|
61
|
+
readonly authenticated: {
|
|
62
|
+
readonly authenticated: true;
|
|
63
|
+
};
|
|
64
|
+
/** Sessions anonymes (aucun utilisateur rattaché). */
|
|
65
|
+
readonly anonymous: {
|
|
66
|
+
readonly authenticated: false;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Ce que l'endpoint de COMPTEURS accepte de filtrer — {@link SESSION_FILTERS}
|
|
71
|
+
* **moins** le champ que ses facettes décomposent (`authenticated`).
|
|
72
|
+
*
|
|
73
|
+
* Le demander ici rendrait une réponse qui se contredit : le total suivrait le
|
|
74
|
+
* filtre pendant que chaque facette l'écraserait par le sien (« 5 sessions au
|
|
75
|
+
* total, dont 40 anonymes »). `user` reste : il découpe une AUTRE dimension, et
|
|
76
|
+
* « combien de sessions pour alice, dont combien d'anonymes ? » est une question
|
|
77
|
+
* cohérente — qui rend d'ailleurs l'ensemble vide, la réponse honnête.
|
|
78
|
+
*
|
|
79
|
+
* Frère de `USER_STATS_FILTERS` / `TOKEN_STATS_FILTERS` / `WEBHOOK_STATS_FILTERS`.
|
|
80
|
+
* Les sessions n'en avaient pas : `authenticated` n'était exposé nulle part, si
|
|
81
|
+
* bien que le trou était fermé par accident plutôt que par décision. Ouvrir le
|
|
82
|
+
* filtre sur la liste — pour que les cartes deviennent cliquables — l'aurait
|
|
83
|
+
* rouvert du même geste.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SESSION_STATS_FILTERS: {
|
|
86
|
+
readonly user: "string";
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Les compteurs rendus par `GET /nodefony/http/api/sessions/stats`.
|
|
90
|
+
*
|
|
91
|
+
* **Dérivé** de {@link SESSION_FACETS} : ajouter une facette ajoute son champ
|
|
92
|
+
* ici, et le front qui ne l'affiche pas ne compile plus. Écrire ce type à la
|
|
93
|
+
* main aurait rendu possible une carte affichant un compteur que le serveur ne
|
|
94
|
+
* calcule pas — ou l'inverse.
|
|
95
|
+
*
|
|
96
|
+
* `users` s'y ajoute à part parce qu'il ne vient pas d'un `COUNT` filtré mais
|
|
97
|
+
* d'une agrégation, que tous les backends ne savent pas rendre.
|
|
98
|
+
*/
|
|
99
|
+
export type ISessionCounts = FacetCounts<typeof SESSION_FACETS> & {
|
|
100
|
+
/** Utilisateurs **distincts** ayant au moins une session (`null` si inconnu). */
|
|
101
|
+
users: FacetCount;
|
|
102
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IPageQuery } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de tri des sessions**, en noms PUBLICS — ceux qu'un client
|
|
4
|
+
* écrit dans l'URL (`?order=updatedAt:DESC`), jamais des noms de colonne.
|
|
5
|
+
*
|
|
6
|
+
* Il vit ici, chez le propriétaire du contrat (`@nodefony/http`), et non dans
|
|
7
|
+
* chaque backend : c'est ce qui garantit qu'une console offre le même tri que
|
|
8
|
+
* l'application tourne sur mémoire, SQLite, PostgreSQL ou Mongo. Un store qui
|
|
9
|
+
* nomme ses colonnes autrement (SQL stocke l'identifiant en `session_id`)
|
|
10
|
+
* **traduit chez lui** — cf {@link SESSION_COLUMN_ALIASES}.
|
|
11
|
+
*
|
|
12
|
+
* - `updatedAt` — dernière activité (l'axe naturel d'une console de sessions).
|
|
13
|
+
* - `createdAt` — ouverture de la session (« qui s'est connecté en premier »).
|
|
14
|
+
* - `user` — porteur, pour regrouper les sessions d'un même compte à l'œil.
|
|
15
|
+
* - `id` — identifiant de session, utile surtout en départage.
|
|
16
|
+
*
|
|
17
|
+
* Ce que la liste n'inclut PAS, et pourquoi : `ip` et `ua` ne sont **pas des
|
|
18
|
+
* champs** mais des entrées du sac de métadonnées (`metaBag`, sérialisé en
|
|
19
|
+
* JSON). Aucun backend ne peut les ordonner sans matérialiser la collection —
|
|
20
|
+
* la console les affiche donc en colonnes non triables, au lieu d'offrir un
|
|
21
|
+
* tri qui serait refusé (SQL) ou coûteux (mémoire).
|
|
22
|
+
*/
|
|
23
|
+
export declare const SESSION_SORTABLE_FIELDS: readonly ["updatedAt", "createdAt", "user", "id"];
|
|
24
|
+
/**
|
|
25
|
+
* Ordre contractuel appliqué quand le client n'en demande aucun : activité la
|
|
26
|
+
* plus récente d'abord, départagée par identifiant pour rester **déterministe**
|
|
27
|
+
* à horodatage égal (sans quoi une pagination peut sauter ou répéter une ligne
|
|
28
|
+
* entre deux pages).
|
|
29
|
+
*/
|
|
30
|
+
export declare const SESSION_DEFAULT_ORDER: NonNullable<IPageQuery["order"]>;
|
|
31
|
+
/**
|
|
32
|
+
* Table d'alias des backends dont le schéma nomme l'identifiant de session
|
|
33
|
+
* `session_id` (SQL comme Mongo) — à passer à `renameOrderFields` (core).
|
|
34
|
+
*
|
|
35
|
+
* C'est une **donnée** du schéma, pas du code : la règle « réécrire les noms »
|
|
36
|
+
* vit au core en un exemplaire, et chaque backend n'apporte que sa table. Sans
|
|
37
|
+
* cette traduction, `?order=id:ASC` partirait vers une colonne `id` inexistante
|
|
38
|
+
* — l'erreur ne se verrait qu'à l'exécution, sur le backend concerné seulement.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SESSION_COLUMN_ALIASES: Readonly<Record<string, string>>;
|
|
41
|
+
/**
|
|
42
|
+
* Ordre par défaut des backends dont le schéma nomme l'identifiant
|
|
43
|
+
* `session_id` — {@link SESSION_DEFAULT_ORDER} déjà traduit.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SESSION_DEFAULT_ORDER_SQL: NonNullable<IPageQuery["order"]>;
|