@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,390 @@
1
+ import Cookie from "../cookies/cookie.js";
2
+ import { extend } from "nodefony";
3
+ import { randomBytes } from "node:crypto";
4
+ //#region nodefony/src/session/session.ts
5
+ const defaultSessionOptions = {
6
+ name: "nodefony",
7
+ strictMode: true,
8
+ refererCheck: false
9
+ };
10
+ /** Taille de l'identifiant opaque (octets CSPRNG → base64url, 43 chars). */
11
+ const SESSION_ID_BYTES = 32;
12
+ /**
13
+ * Session serveur Nodefony — état persistant lié à une identité, indexé par un
14
+ * identifiant **opaque** (porté par le cookie) dans un {@link ISessionStorage}
15
+ * pluggable (File / Redis / SQL). Modèle BFF : le cookie ne transporte qu'un
16
+ * secret aléatoire, jamais de données ni de JWT.
17
+ *
18
+ * Objet **léger** : trois sacs `{}` (attributs / métas / flash)
19
+ * au lieu d'un `Container` DI par session.
20
+ *
21
+ * Dirty-tracking : toute mutation (`set` / `setFlashBag` / `setMetaBag` /
22
+ * `getFlashBag` qui consomme / …) lève {@link dirty} ; {@link save} n'écrit dans
23
+ * le storage **que** si `dirty`. Une requête qui ne touche pas la session ne
24
+ * déclenche aucune écriture (supprime la contention `write`/requête).
25
+ */
26
+ var Session = class {
27
+ id = "";
28
+ name = "";
29
+ status = "none";
30
+ storage;
31
+ manager;
32
+ saved = false;
33
+ migrated = false;
34
+ /**
35
+ * Lecture seule (intent `@UseSession({ readOnly })`) : la session est reprise et
36
+ * lue mais **jamais persistée** — {@link save} devient un no-op. Différenciateur
37
+ * perf : une route qui ne fait que LIRE la session (afficher le user…) ne paie
38
+ * aucune écriture storage.
39
+ */
40
+ readOnly = false;
41
+ context;
42
+ created;
43
+ updated;
44
+ options;
45
+ cookieSession = null;
46
+ lifetime;
47
+ user;
48
+ strategy;
49
+ /** Sac d'attributs applicatifs (clé/valeur) — exposé via {@link getAttributes}. */
50
+ attributesBag = {};
51
+ /** Sac de métadonnées techniques (host, ip, ua…) — exposé via {@link getMetas}. */
52
+ metaBagStore = {};
53
+ /** Sac de messages flash (consommés à la lecture). Public : lu par les storages. */
54
+ flashBag = {};
55
+ /** Drapeau dirty-tracking — lu via le getter {@link dirty}. */
56
+ mutated = false;
57
+ constructor(name, options, manager) {
58
+ this.options = extend({}, defaultSessionOptions, options);
59
+ this.manager = manager;
60
+ this.storage = this.manager.storage;
61
+ if (!this.storage) this.status = "disabled";
62
+ this.setName(name);
63
+ this.strategy = this.manager.sessionStrategy;
64
+ }
65
+ /** Vrai si la session a été mutée sans être encore persistée (dirty-tracking). */
66
+ get dirty() {
67
+ return this.mutated;
68
+ }
69
+ log(pci, severity) {
70
+ this.manager.log(pci, severity, `SESSION ${this.name}`);
71
+ }
72
+ /**
73
+ * Démarre (ou reprend) la session. Cookie présent → reprise depuis le storage ;
74
+ * sinon → nouvelle session.
75
+ *
76
+ * @param context - contexte HTTP/HTTP2/WS courant.
77
+ */
78
+ async start(context) {
79
+ this.context = context;
80
+ const ret = this.checkStatus();
81
+ if (ret === false) return this;
82
+ if (ret === "restart") return this.start(context);
83
+ return this.getSession();
84
+ }
85
+ /**
86
+ * Lit l'identifiant opaque du cookie puis reprend la session, ou en crée une
87
+ * neuve si aucun cookie. Cookie-only : aucun identifiant lu depuis l'URL.
88
+ */
89
+ async getSession() {
90
+ if (this.context?.cookieSession) {
91
+ this.id = this.context.cookieSession.value;
92
+ this.cookieSession = this.context.cookieSession;
93
+ }
94
+ if (this.id) return this.resume();
95
+ this.clear();
96
+ return this.create(this.lifetime ?? 0);
97
+ }
98
+ /**
99
+ * Reprend la session `id` depuis le storage. Invalide (→ session neuve) si
100
+ * introuvable en strict mode, ou expirée/illégitime.
101
+ */
102
+ async resume() {
103
+ const data = await this.storage.start(this.id);
104
+ if (data && Object.keys(data).length) {
105
+ this.deSerialize(data);
106
+ if (!this.isValidSession(data, this.context)) {
107
+ this.log(`INVALID SESSION ==> ${this.name} : ${this.id}`, "WARNING");
108
+ await this.invalidate();
109
+ return this;
110
+ }
111
+ this.status = "active";
112
+ return this;
113
+ }
114
+ if (this.options.strictMode) {
115
+ this.log(`SESSION strict_mode unknown id ==> ${this.name}`, "DEBUG");
116
+ await this.invalidate();
117
+ return this;
118
+ }
119
+ this.status = "active";
120
+ return this;
121
+ }
122
+ /**
123
+ * Crée une session neuve : identifiant opaque CSPRNG, cookie, métadonnées.
124
+ * Marquée `dirty` (sauf `saveUninitialized:false`) → persistée + `Set-Cookie`
125
+ * au prochain {@link save}.
126
+ */
127
+ create(lifetime, id, settingsCookie = {}) {
128
+ this.id = id || this.generateId();
129
+ const settings = extend({}, this.options.cookie, settingsCookie);
130
+ this.log(`NEW SESSION CREATE : ${this.id}`, "DEBUG");
131
+ this.cookieSession = this.setCookieSession(lifetime, settings);
132
+ this.setMetasSession(settings);
133
+ this.status = "active";
134
+ this.mutated = true;
135
+ return this;
136
+ }
137
+ /** Génère un identifiant de session opaque (32 octets CSPRNG → base64url). */
138
+ generateId() {
139
+ return randomBytes(SESSION_ID_BYTES).toString("base64url");
140
+ }
141
+ /**
142
+ * Régénère l'identifiant (nouveau secret opaque) en conservant l'état courant.
143
+ * Anti session-fixation (OWASP) — appelée à chaque ouverture de session
144
+ * authentifiée par `AuthFlow.#openSession()` (`@nodefony/security`), qui
145
+ * détruit ensuite l'ancienne entrée de storage. Repositionne le cookie et
146
+ * marque la session `dirty`.
147
+ */
148
+ regenerateId() {
149
+ this.id = this.generateId();
150
+ this.mutated = true;
151
+ if (this.context?.response) this.cookieSession = this.setCookieSession(this.lifetime ?? 0, this.options.cookie ?? {});
152
+ }
153
+ /**
154
+ * Persiste la session **si elle est dirty** (sinon no-op). Réécrit le blob
155
+ * sérialisé dans le storage, repositionne created/updated, lève l'événement
156
+ * `onSaveSession`.
157
+ *
158
+ * @param user - principal authentifié (string) lié au blob ; défaut courant.
159
+ */
160
+ async save(user) {
161
+ if (this.readOnly) {
162
+ if (this.mutated) this.log(`READONLY SESSION mutated — write skipped : ${this.name}`, "WARNING");
163
+ return this;
164
+ }
165
+ if (!this.mutated) return this;
166
+ const stored = await this.storage.write(this.id, this.serialize(user));
167
+ this.created = stored.createdAt ?? this.created;
168
+ this.updated = stored.updatedAt ?? this.updated;
169
+ this.mutated = false;
170
+ this.saved = true;
171
+ if (this.context) await this.context.fireAsync("onSaveSession", this);
172
+ return this;
173
+ }
174
+ /**
175
+ * Détruit la session courante (storage) et en recrée une neuve (nouvel
176
+ * identifiant + cookie). État applicatif réinitialisé.
177
+ */
178
+ async invalidate(lifetime = this.lifetime ?? 0, id, settingsCookie = {}) {
179
+ this.log(`INVALIDATE SESSION ==> ${this.name} : ${this.id}`, "DEBUG");
180
+ const oldId = this.id;
181
+ this.clear();
182
+ await this.storage.destroy(oldId);
183
+ return this.create(lifetime, id, settingsCookie);
184
+ }
185
+ /**
186
+ * Détruit la session : vide les sacs, supprime l'entrée storage, et (option)
187
+ * efface le cookie.
188
+ */
189
+ async destroy(cookieDelete = false) {
190
+ this.clear();
191
+ await this.storage.destroy(this.id);
192
+ this.mutated = false;
193
+ this.status = "disabled";
194
+ if (cookieDelete) this.deleteCookieSession();
195
+ return true;
196
+ }
197
+ setCookieSession(leftTime, options = {}) {
198
+ if (this.context && this.context.response) {
199
+ const settings = extend({}, this.options.cookie, options);
200
+ if (leftTime) settings.maxAge = leftTime;
201
+ const cookie = new Cookie(this.context.getSessionCookieName(), this.id, settings);
202
+ this.context.response.addCookie(cookie);
203
+ this.cookieSession = cookie;
204
+ this.context.cookieSession = cookie;
205
+ return cookie;
206
+ }
207
+ return null;
208
+ }
209
+ deleteCookieSession() {
210
+ if (this.context && this.context.response) {
211
+ let cookie = this.cookieSession;
212
+ if (cookie) cookie.expires = /* @__PURE__ */ new Date(0);
213
+ else cookie = new Cookie(this.context.getSessionCookieName(), "", { expires: /* @__PURE__ */ new Date(0) });
214
+ this.context.response.setCookie(cookie);
215
+ this.cookieSession = null;
216
+ this.context.cookieSession = null;
217
+ return cookie;
218
+ }
219
+ return this.cookieSession;
220
+ }
221
+ isValidSession(_data, context) {
222
+ if (this.options.refererCheck) try {
223
+ return this.checkSecureReferer(context);
224
+ } catch {
225
+ this.log(`SESSION REFERER MISMATCH ==> ${this.name} : ${this.id}`, "WARNING");
226
+ return false;
227
+ }
228
+ const now = Date.now();
229
+ if (this.options.absoluteTimeoutS && this.created) {
230
+ if (now - new Date(this.created).getTime() > this.options.absoluteTimeoutS * 1e3) {
231
+ this.log(`SESSION EXPIRED (absolute) ==> ${this.name} : ${this.id}`, "WARNING");
232
+ return false;
233
+ }
234
+ }
235
+ const idleS = this.options.idleTimeoutS;
236
+ if (idleS && idleS > 0 && this.updated) {
237
+ const lastUsed = new Date(this.updated).getTime();
238
+ if (lastUsed && lastUsed + idleS * 1e3 < now) {
239
+ this.log(`SESSION EXPIRED (idle) ==> ${this.name} : ${this.id}`, "WARNING");
240
+ return false;
241
+ }
242
+ }
243
+ return true;
244
+ }
245
+ /**
246
+ * Prolonge l'idle timeout de la session active sur l'activité courante, **de
247
+ * façon throttlée** (1 écriture par tranche d'idle, jamais par requête) et
248
+ * **sans réécrire le blob** (write léger `touch` du storage) — l'activité
249
+ * HTTP/WS réelle, **y compris en lecture seule**, empêche l'expiration d'une
250
+ * session utilisée (NIST/OWASP : idle « since the last request »). N'affecte
251
+ * **jamais** l'absolute timeout (borné à la création).
252
+ *
253
+ * No-op si : pas active, pas d'entrée storage à prolonger (`!updated`), idle
254
+ * désactivé, store sans `touch`, ou dernière activité trop récente (throttle =
255
+ * mi-vie de l'idle). Met à jour `updated` localement → le throttle compte
256
+ * depuis ce touch.
257
+ */
258
+ async touchIfNeeded() {
259
+ if (this.status !== "active") return;
260
+ const idleS = this.options.idleTimeoutS;
261
+ if (!idleS || idleS <= 0) return;
262
+ const storage = this.storage;
263
+ if (typeof storage.touch !== "function") return;
264
+ if (!this.updated) return;
265
+ const now = Date.now();
266
+ const lastUsed = new Date(this.updated).getTime();
267
+ if (lastUsed && now - lastUsed < idleS * 1e3 / 2) return;
268
+ await storage.touch(this.id, idleS);
269
+ this.updated = new Date(now);
270
+ }
271
+ checkSecureReferer(context) {
272
+ const host = context.getHost();
273
+ const meta = this.getMetaBag("host");
274
+ if (host === meta) return true;
275
+ this.log(`SESSION REFERER NOT SAME, HOST: ${host} META: ${String(meta)}`, "WARNING");
276
+ throw new Error("session referer mismatch");
277
+ }
278
+ setMetasSession(cookieSetting = {}) {
279
+ this.setMetaBag("lifetime", cookieSetting.maxAge ?? this.options.cookie?.maxAge);
280
+ const ctx = this.context;
281
+ this.setMetaBag("request", ctx?.type);
282
+ try {
283
+ this.setMetaBag("remoteAddress", ctx?.getRemoteAddress());
284
+ this.setMetaBag("host", ctx?.getHost());
285
+ this.setMetaBag("user_agent", ctx?.getUserAgent() || "Not Defined");
286
+ } catch (e) {
287
+ this.log(e, "DEBUG");
288
+ }
289
+ }
290
+ get(key) {
291
+ const v = this.attributesBag[key];
292
+ return v === void 0 ? null : v;
293
+ }
294
+ set(key, value) {
295
+ this.attributesBag[key] = value;
296
+ this.mutated = true;
297
+ return value;
298
+ }
299
+ getAttributes() {
300
+ return this.attributesBag;
301
+ }
302
+ getMetaBag(key) {
303
+ const v = this.metaBagStore[key];
304
+ return v === void 0 ? null : v;
305
+ }
306
+ setMetaBag(key, value) {
307
+ this.metaBagStore[key] = value;
308
+ this.mutated = true;
309
+ }
310
+ getMetas() {
311
+ return this.metaBagStore;
312
+ }
313
+ getFlashBag(key) {
314
+ const v = this.flashBag[key];
315
+ if (v !== void 0) {
316
+ delete this.flashBag[key];
317
+ this.mutated = true;
318
+ return v;
319
+ }
320
+ return null;
321
+ }
322
+ setFlashBag(key, value) {
323
+ if (!key) throw new Error(`FlashBag key must be define : ${key}`);
324
+ this.flashBag[key] = value;
325
+ this.mutated = true;
326
+ return value;
327
+ }
328
+ flashBags() {
329
+ return this.flashBag;
330
+ }
331
+ clearFlashBag(key) {
332
+ if (!key) throw new Error(`clearFlashBag key must be define : ${key}`);
333
+ if (this.flashBag[key] !== void 0) {
334
+ delete this.flashBag[key];
335
+ this.mutated = true;
336
+ }
337
+ }
338
+ clearFlashBags() {
339
+ this.flashBag = {};
340
+ this.mutated = true;
341
+ }
342
+ serialize(user) {
343
+ return {
344
+ Attributes: this.attributesBag,
345
+ metaBag: this.metaBagStore,
346
+ flashBag: this.flashBag,
347
+ user: user ?? this.user ?? ""
348
+ };
349
+ }
350
+ deSerialize(data) {
351
+ if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
352
+ if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
353
+ if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
354
+ this.created = data.createdAt ?? this.created;
355
+ this.updated = data.updatedAt ?? this.updated;
356
+ if (data.user) this.user = data.user;
357
+ }
358
+ /** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
359
+ clear() {
360
+ this.attributesBag = {};
361
+ this.metaBagStore = {};
362
+ this.flashBag = {};
363
+ }
364
+ getName() {
365
+ return this.name;
366
+ }
367
+ setName(name) {
368
+ this.name = name || this.options.name;
369
+ }
370
+ checkStatus() {
371
+ switch (this.status) {
372
+ case "active":
373
+ this.log(`SESSION ALREADY STARTED ==> ${this.name} : ${this.id}`, "WARNING");
374
+ return false;
375
+ case "disabled": {
376
+ const storage = this.manager.initializeStorage();
377
+ if (storage) {
378
+ this.storage = storage;
379
+ this.status = "none";
380
+ return "restart";
381
+ }
382
+ this.log("SESSION STORAGE HANDLER NOT FOUND", "ERROR");
383
+ throw new Error("SESSION STORAGE HANDLER NOT FOUND");
384
+ }
385
+ default: return true;
386
+ }
387
+ }
388
+ };
389
+ //#endregion
390
+ export { Session as default };
@@ -0,0 +1,185 @@
1
+ import { SESSION_DEFAULT_ORDER, SESSION_SORTABLE_FIELDS } from "./sessionSort.js";
2
+ import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
3
+ //#region nodefony/src/session/storage/MemorySessionStorage.ts
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
+ var MemorySessionStorage = class {
20
+ manager;
21
+ idleTimeoutS;
22
+ absoluteTimeoutS;
23
+ /** id → session sérialisée (source de vérité, horodatages inclus). */
24
+ #sessions = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Trie sur tout le vocabulaire public : les données sont déjà en RAM, aucun
27
+ * champ n'est plus coûteux qu'un autre. Aucune traduction — les clés internes
28
+ * portent déjà ces noms (`id` étant la clé de la Map).
29
+ */
30
+ sortableFields = SESSION_SORTABLE_FIELDS;
31
+ constructor(manager) {
32
+ this.manager = manager;
33
+ this.idleTimeoutS = manager.options.idleTimeoutS;
34
+ this.absoluteTimeoutS = manager.options.absoluteTimeoutS;
35
+ }
36
+ /** Lecture par id — copie superficielle (le consommateur ne mute pas le store). */
37
+ read(id) {
38
+ const stored = this.#sessions.get(id);
39
+ return Promise.resolve(stored ? { ...stored } : {});
40
+ }
41
+ start(id) {
42
+ return this.read(id);
43
+ }
44
+ /**
45
+ * Écrit (upsert) le blob. `createdAt` est FIXÉ à la création et préservé aux
46
+ * updates (borne absolute) ; `updatedAt` est posé à chaque écriture (borne idle).
47
+ */
48
+ write(id, data) {
49
+ const now = /* @__PURE__ */ new Date();
50
+ const existing = this.#sessions.get(id);
51
+ const record = {
52
+ ...data,
53
+ createdAt: existing?.createdAt ?? data.createdAt ?? now,
54
+ updatedAt: now
55
+ };
56
+ this.#sessions.set(id, record);
57
+ return Promise.resolve(data);
58
+ }
59
+ /** Compte des sessions présentes (+ passe GC comme les autres stores à l'open). */
60
+ async open() {
61
+ await this.gc();
62
+ this.manager.log(`SESSIONS STORAGE ==> ${this.manager.options.store.toUpperCase()} COUNT SESSIONS : ${this.#sessions.size}`);
63
+ return this.#sessions.size;
64
+ }
65
+ close() {
66
+ return true;
67
+ }
68
+ destroy(id) {
69
+ this.#sessions.delete(id);
70
+ return Promise.resolve(true);
71
+ }
72
+ /**
73
+ * Prolonge l'idle (timeout glissant) : rafraîchit `updatedAt` SANS toucher
74
+ * `createdAt` (borne absolute intacte). Session absente (purgée) → no-op.
75
+ */
76
+ touch(id) {
77
+ const stored = this.#sessions.get(id);
78
+ if (stored) stored.updatedAt = /* @__PURE__ */ new Date();
79
+ return Promise.resolve();
80
+ }
81
+ /**
82
+ * Purge idle (inactivité depuis `updatedAt`) ET absolute (âge depuis `createdAt`,
83
+ * jamais prolongé). Une borne à 0 = désactivée. Déterministe (synchrone).
84
+ */
85
+ gc(idleSeconds, absoluteSeconds) {
86
+ const idleMs = (idleSeconds ?? this.idleTimeoutS) * 1e3;
87
+ const absoluteMs = (absoluteSeconds ?? this.absoluteTimeoutS) * 1e3;
88
+ const now = Date.now();
89
+ let deleted = 0;
90
+ for (const [id, s] of this.#sessions) {
91
+ const updated = s.updatedAt ? s.updatedAt.getTime() : now;
92
+ const created = s.createdAt ? s.createdAt.getTime() : now;
93
+ const idleExpired = idleMs > 0 && updated + idleMs < now;
94
+ const absoluteExpired = absoluteMs > 0 && created + absoluteMs < now;
95
+ if (idleExpired || absoluteExpired) {
96
+ this.#sessions.delete(id);
97
+ deleted++;
98
+ }
99
+ }
100
+ if (deleted > 0) this.manager.log(`MEMORY SESSIONS STORAGE GARBAGE COLLECTOR ==> ${deleted} DELETED`);
101
+ return Promise.resolve();
102
+ }
103
+ /** Énumération admin — filtre `user` appliqué en mémoire. */
104
+ listAll(filter) {
105
+ const records = [];
106
+ for (const [id, data] of this.#sessions) {
107
+ if (filter?.user !== void 0 && data.user !== filter.user) continue;
108
+ records.push({
109
+ id,
110
+ data: { ...data }
111
+ });
112
+ }
113
+ return Promise.resolve(records);
114
+ }
115
+ /**
116
+ * `true` si l'entrée passe les filtres — prédicat partagé par {@link listPage}
117
+ * et {@link countSessions} (une seule définition du périmètre : compter et
118
+ * lister ne peuvent pas diverger).
119
+ */
120
+ #matches(data, query) {
121
+ if (!query) return true;
122
+ if (query.user !== void 0 && data.user !== query.user) return false;
123
+ if (query.authenticated !== void 0) {
124
+ if (!!data.user !== query.authenticated) return false;
125
+ }
126
+ return true;
127
+ }
128
+ /**
129
+ * Pagination **offset** avec `total` exact, ordre `updatedAt` DESC (id ASC en
130
+ * départage). Les données étant déjà en RAM par conception, le coût par requête
131
+ * est celui du tri des **références** filtrées — aucune copie de blob n'est
132
+ * faite hors de la page rendue.
133
+ *
134
+ * **Redaction par construction** (garantie du contrat, pas une optimisation) :
135
+ * `Attributes`/`flashBag` sortent VIDES, comme chez les stores SQL/NoSQL qui ne
136
+ * les SELECTent pas. Ici c'est gratuit — on ne recopie simplement pas ces deux
137
+ * bags — et ça aligne le store mémoire sur la même garantie : un record
138
+ * d'énumération admin ne porte jamais de donnée métier.
139
+ */
140
+ listPage(query) {
141
+ assertPageQuery(query, "offset");
142
+ const limit = Math.max(0, query.limit);
143
+ const offset = Math.max(0, query.offset ?? 0);
144
+ const matched = [];
145
+ for (const entry of this.#sessions) if (this.#matches(entry[1], query)) matched.push(entry);
146
+ const order = pickOrder(query.order, this.sortableFields, SESSION_DEFAULT_ORDER);
147
+ matched.sort(compareByOrder(order, ([id, data], field) => field === "id" ? id : data[field]));
148
+ const items = matched.slice(offset, offset + limit).map(([id, data]) => ({
149
+ id,
150
+ data: {
151
+ ...data,
152
+ Attributes: {},
153
+ flashBag: {}
154
+ }
155
+ }));
156
+ return Promise.resolve({
157
+ items,
158
+ total: query.withTotal === false ? void 0 : matched.length,
159
+ limit: query.limit,
160
+ offset,
161
+ hasNext: offset + items.length < matched.length
162
+ });
163
+ }
164
+ /** `COUNT` filtré — parcourt sans allouer (aucun record matérialisé). */
165
+ countSessions(query) {
166
+ let count = 0;
167
+ for (const data of this.#sessions.values()) if (this.#matches(data, query)) count++;
168
+ return Promise.resolve(count);
169
+ }
170
+ /**
171
+ * `COUNT(DISTINCT user)` en mémoire. Le `Set` est alloué à l'appel et relâché
172
+ * aussitôt : c'est un chemin d'administration, appelé à l'ouverture d'un
173
+ * écran, jamais dans le pipeline de requête.
174
+ */
175
+ countDistinctUsers(query) {
176
+ const users = /* @__PURE__ */ new Set();
177
+ for (const data of this.#sessions.values()) {
178
+ if (!this.#matches(data, query)) continue;
179
+ if (typeof data.user === "string" && data.user) users.add(data.user);
180
+ }
181
+ return Promise.resolve(users.size);
182
+ }
183
+ };
184
+ //#endregion
185
+ export { MemorySessionStorage as default };