@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4

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 (76) hide show
  1. package/README.md +4 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +15 -20
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  6. package/dist/nodefony/controller/AdminApiController.js +45 -11
  7. package/dist/nodefony/controller/ApiKeyController.js +18 -10
  8. package/dist/nodefony/controller/BenchController.js +1 -2
  9. package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
  10. package/dist/nodefony/controller/LoginPageController.js +117 -0
  11. package/dist/nodefony/controller/OAuth2Controller.js +133 -17
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +50 -13
  14. package/dist/nodefony/controller/TokenAuthController.js +14 -8
  15. package/dist/nodefony/controller/TotpController.js +7 -8
  16. package/dist/nodefony/controller/WebAuthnController.js +28 -12
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +268 -65
  19. package/dist/nodefony/service/AdminBroker.js +4 -6
  20. package/dist/nodefony/service/Eta.js +1 -2
  21. package/dist/nodefony/service/IdempotencyStore.js +4 -6
  22. package/dist/nodefony/service/router.js +97 -34
  23. package/dist/nodefony/src/Controller.js +71 -43
  24. package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
  25. package/dist/nodefony/src/KernelAdminApi.js +82 -18
  26. package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
  27. package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
  28. package/dist/nodefony/src/Resolver.js +105 -36
  29. package/dist/nodefony/src/ResourceController.js +2 -3
  30. package/dist/nodefony/src/Route.js +54 -8
  31. package/dist/nodefony/src/SecurityApiController.js +23 -0
  32. package/dist/nodefony/src/SyslogAdminApi.js +1 -2
  33. package/dist/nodefony/src/Template.js +1 -2
  34. package/dist/nodefony/src/configMutation.js +1 -2
  35. package/dist/nodefony/src/docsReader.js +39 -23
  36. package/dist/nodefony/src/idempotency.js +14 -22
  37. package/dist/nodefony/src/idempotencyGc.js +1 -2
  38. package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
  39. package/dist/nodefony/src/loginPage.js +430 -0
  40. package/dist/nodefony/src/requireAuthentication.js +35 -0
  41. package/dist/nodefony/src/routeSchema.js +81 -0
  42. package/dist/nodefony/src/scopeCatalog.js +1 -2
  43. package/dist/nodefony/src/singletonGuard.js +1 -2
  44. package/dist/nodefony/src/syslogFilters.js +1 -2
  45. package/dist/types/index.d.ts +8 -12
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
  47. package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
  48. package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
  49. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
  50. package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
  51. package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
  52. package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
  53. package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
  54. package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
  55. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  56. package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
  57. package/dist/types/nodefony/service/router.d.ts +40 -1
  58. package/dist/types/nodefony/src/Controller.d.ts +54 -17
  59. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  60. package/dist/types/nodefony/src/Resolver.d.ts +44 -1
  61. package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
  62. package/dist/types/nodefony/src/Route.d.ts +21 -1
  63. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
  64. package/dist/types/nodefony/src/docsReader.d.ts +10 -6
  65. package/dist/types/nodefony/src/idempotency.d.ts +12 -10
  66. package/dist/types/nodefony/src/loginPage.d.ts +144 -0
  67. package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
  68. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  69. package/docs/admin.md +83 -78
  70. package/docs/controller.md +112 -111
  71. package/docs/decorateurs.md +212 -163
  72. package/docs/idempotence.md +137 -135
  73. package/docs/routing.md +96 -95
  74. package/docs/templates.md +55 -55
  75. package/package.json +10 -13
  76. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,7 +1,7 @@
1
1
  import Controller from "./Controller.js";
2
2
  import { assertPageQuery } from "nodefony";
3
3
  import { HttpError } from "@nodefony/http";
4
- //#region nodefony/src/ResourceController.ts
4
+ // nodefony/src/ResourceController.ts
5
5
  /**
6
6
  * Controller de ressource **souverain** (V4.2 — POC API souveraine, Phase 2) :
7
7
  * la logique métier est écrite UNE fois dans le service de ressource, les
@@ -22,7 +22,7 @@ import { HttpError } from "@nodefony/http";
22
22
  * sur `this` doit rétrograder : `static scope = "request"`.
23
23
  *
24
24
  * Sécurité : aucun critère de requête n'est passé AUTOMATIQUEMENT au service
25
- * (pas de `find(this.queryGet)` implicite) — exposer un filtrage est une
25
+ * (pas de `find(this.query)` implicite) — exposer un filtrage est une
26
26
  * décision EXPLICITE de la sous-classe (deny-by-default ; le scope de
27
27
  * sécurité des données se branche au niveau service/criteria, P6).
28
28
  *
@@ -144,5 +144,4 @@ var ResourceController = class extends Controller {
144
144
  return Promise.resolve(resource.delete(criteria));
145
145
  }
146
146
  };
147
- //#endregion
148
147
  export { ResourceController as default };
@@ -1,7 +1,7 @@
1
1
  import { escapeRegExp, stripTrailingSlashes, typeOf } from "nodefony";
2
2
  import { HttpError, compileDomainPatterns, isDomainAllowed } from "@nodefony/http";
3
3
  import { createHash } from "node:crypto";
4
- //#region nodefony/src/Route.ts
4
+ // nodefony/src/Route.ts
5
5
  /**
6
6
  * Motif d'un segment variable : `{id}`, `/{slug}?`, `.{format}`, `{id}(\d+)`.
7
7
  *
@@ -99,6 +99,32 @@ function replaceCallback(_match, slash, dot, key, capture, opt, _offset) {
99
99
  }
100
100
  throw new Error(`Bad path `);
101
101
  }
102
+ /**
103
+ * Exigences d'une route, l'option `method` versée dans `requirements.methods`.
104
+ *
105
+ * Seul `requirements.methods` restreint le match ; l'option `method` n'était
106
+ * lue que pour l'affichage, si bien qu'une route « POST » servait toutes les
107
+ * méthodes. Les deux déclarations s'UNISSENT, comme le font les décorateurs
108
+ * `@Get`/`@Post` : déclarer n'élargit jamais en silence ni n'écrase.
109
+ *
110
+ * @param requirements - exigences déclarées (jamais mutées)
111
+ * @param method - méthode(s) de l'option `method`
112
+ * @returns les exigences à poser sur la route
113
+ */
114
+ function withDeclaredMethod(requirements, method) {
115
+ if (method === void 0) return requirements ?? {};
116
+ const own = requirements?.methods;
117
+ const union = new Set(Array.isArray(method) ? method : [method]);
118
+ if (own !== void 0) for (const m of Array.isArray(own) ? own : [own]) union.add(m);
119
+ return {
120
+ ...requirements,
121
+ methods: [...union]
122
+ };
123
+ }
124
+ /** Canal chiffré : `https` (HTTP) ou `wss` (WebSocket). */
125
+ function isSecureScheme(scheme) {
126
+ return scheme === "https" || scheme === "wss";
127
+ }
102
128
  var Route = class Route {
103
129
  name;
104
130
  path;
@@ -106,7 +132,6 @@ var Route = class Route {
106
132
  classMethod;
107
133
  prefix;
108
134
  method;
109
- schemes;
110
135
  pattern;
111
136
  variables = [];
112
137
  /**
@@ -144,6 +169,8 @@ var Route = class Route {
144
169
  * pas son rôle par défaut. Cf {@link RouteOptions.areaRoleExempt}.
145
170
  */
146
171
  areaRoleExempt = false;
172
+ /** Route de repli, résolue après toutes les autres. Cf {@link RouteOptions.fallback}. */
173
+ fallback = false;
147
174
  /**
148
175
  * P2.9 — Cache mémoïsé : l'action attend-elle le **flux brut** du body
149
176
  * (`@Body({ stream:true })`) ? `undefined` = pas encore calculé (résolu au 1er
@@ -177,9 +204,10 @@ var Route = class Route {
177
204
  this.method = obj.method;
178
205
  this.setHostname(obj.host);
179
206
  this.setDefaults(obj.defaults);
180
- this.requirements = obj.requirements ?? {};
207
+ this.requirements = withDeclaredMethod(obj.requirements, obj.method);
181
208
  this.bypassFirewall = obj.bypassFirewall ?? false;
182
209
  this.areaRoleExempt = obj.areaRoleExempt ?? false;
210
+ this.fallback = obj.fallback ?? false;
183
211
  this.compile();
184
212
  }
185
213
  this.generateId();
@@ -238,7 +266,7 @@ var Route = class Route {
238
266
  const k = this.variables[i] || "wildcard";
239
267
  param &&= decode(param);
240
268
  const req = this.getRequirement(k);
241
- let result = null;
269
+ let result;
242
270
  if (req) {
243
271
  let compiled = this.varRegexp?.[k];
244
272
  if (!compiled) {
@@ -366,7 +394,8 @@ var Route = class Route {
366
394
  const action = this.classMethod || this.name;
367
395
  const mod = this.module?.name ? `@${this.module.name}/` : "";
368
396
  const auth = this.bypassFirewall ? " (no auth)" : this.areaRoleExempt ? " (hors rôle de zone)" : "";
369
- return `${method} ${this.path} → ${mod}${ctrl}.${action}${auth}`;
397
+ const fallback = this.fallback ? " (repli)" : "";
398
+ return `${method} ${this.path} → ${mod}${ctrl}.${action}${auth}${fallback}`;
370
399
  }
371
400
  toObject() {
372
401
  return {
@@ -376,10 +405,11 @@ var Route = class Route {
376
405
  host: this.host,
377
406
  controller: this.defaults.controller,
378
407
  filePath: this.filePath,
379
- schemes: this.schemes,
408
+ schemes: this.requirements.scheme,
380
409
  variables: this.variables,
381
410
  bypassFirewall: this.bypassFirewall,
382
- areaRoleExempt: this.areaRoleExempt
411
+ areaRoleExempt: this.areaRoleExempt,
412
+ fallback: this.fallback
383
413
  };
384
414
  }
385
415
  setDefaults(arg) {
@@ -459,6 +489,13 @@ var Route = class Route {
459
489
  return Object.keys(this.requirements).length;
460
490
  }
461
491
  matchRequirements(context, methodOverride) {
492
+ if (this.requirements.methods === void 0 && (methodOverride !== void 0 || context.method === "WEBSOCKET")) {
493
+ const error = new HttpError(`Method ${methodOverride ?? "WEBSOCKET"} Not Allowed`);
494
+ error.code = 405;
495
+ error.type = "method";
496
+ error.allow = "";
497
+ throw error;
498
+ }
462
499
  if (this.hasRequirements()) for (const i in this.requirements) switch (i) {
463
500
  case "methods":
464
501
  if (!this.methodsSet) throw new Error(`Bad config route method : ${String(this.requirements.methods)}`);
@@ -480,6 +517,16 @@ var Route = class Route {
480
517
  throw error;
481
518
  }
482
519
  break;
520
+ case "scheme": {
521
+ const required = this.requirements.scheme;
522
+ if (required !== void 0 && isSecureScheme(required) !== isSecureScheme(context.scheme)) {
523
+ const error = new HttpError(`Scheme ${context.scheme} Forbidden for this route : ${required} required`);
524
+ error.code = 403;
525
+ error.type = "scheme";
526
+ throw error;
527
+ }
528
+ break;
529
+ }
483
530
  case "domain": break;
484
531
  case "protocol": if (context.method === "WEBSOCKET") {
485
532
  const requirement = this.requirements[i];
@@ -502,5 +549,4 @@ var Route = class Route {
502
549
  return true;
503
550
  }
504
551
  };
505
- //#endregion
506
552
  export { Route as default };
@@ -0,0 +1,23 @@
1
+ import Controller from "./Controller.js";
2
+ // nodefony/src/SecurityApiController.ts
3
+ const NO_STORE = { "cache-control": "no-store" };
4
+ /**
5
+ * Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
6
+ * réponse JSON part en `Cache-Control: no-store`.
7
+ *
8
+ * Ces réponses portent une identité, un jeton, un secret affiché une seule
9
+ * fois (clé API, secret TOTP, codes de récupération) ou leur refus. Un cookie
10
+ * ou un en-tête dans la requête n'empêche pas un cache partagé de STOCKER la
11
+ * réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton. La règle
12
+ * vit ici, une fois : une action ajoutée à un contrôleur héritier ne peut pas
13
+ * l'oublier. Un en-tête passé par l'action garde la priorité.
14
+ */
15
+ var SecurityApiController = class extends Controller {
16
+ renderJson(obj, status, headers) {
17
+ return super.renderJson(obj, status, {
18
+ ...NO_STORE,
19
+ ...headers
20
+ });
21
+ }
22
+ };
23
+ export { SecurityApiController as default };
@@ -2,7 +2,7 @@ import { SYSLOG_FILTERS, SYSLOG_SORTABLE } from "./syslogFilters.js";
2
2
  import path from "node:path";
3
3
  import { Syslog, getActiveLogDriver, getLogDriver, listLogDrivers, parseFilters, parsePageQuery, redactSecrets, setActiveLogDriver } from "nodefony";
4
4
  import fsp from "node:fs/promises";
5
- //#region nodefony/src/SyslogAdminApi.ts
5
+ // nodefony/src/SyslogAdminApi.ts
6
6
  /** Plafond d'octets lus en queue de fichier (tail / fenêtre incrémentale). */
7
7
  const MAX_TAIL_BYTES = 262144;
8
8
  /**
@@ -462,5 +462,4 @@ function createSyslogAdminApi(syslog, options = {}) {
462
462
  adminEndpoints: () => endpoints
463
463
  };
464
464
  }
465
- //#endregion
466
465
  export { createSyslogAdminApi };
@@ -1,5 +1,5 @@
1
1
  import { Service } from "nodefony";
2
- //#region nodefony/src/Template.ts
2
+ // nodefony/src/Template.ts
3
3
  var Template = class extends Service {
4
4
  engine;
5
5
  module;
@@ -11,5 +11,4 @@ var Template = class extends Service {
11
11
  this.cache = module.kernel?.environment === "prod" || module.kernel?.environment === "production";
12
12
  }
13
13
  };
14
- //#endregion
15
14
  export { Template as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/configMutation.ts
1
+ // nodefony/src/configMutation.ts
2
2
  /** Narrowing défensif : `unknown` → nœud JSON Schema (ou `null` si non-objet). */
3
3
  function asNode(value) {
4
4
  if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
@@ -182,5 +182,4 @@ function getResolvedPath(obj, segments) {
182
182
  }
183
183
  return cur;
184
184
  }
185
- //#endregion
186
185
  export { getResolvedPath, navigateSchemaNode, nodeFlags, notEditableReason, recipeFor, validateLeafValue };
@@ -1,11 +1,11 @@
1
1
  import { basename, dirname, extname, join } from "node:path";
2
- import { findProjectRoot, packageManagerCommandLines, packageManagerExecArgs, portableSpawn, resolvePackageManager, resolveSymbolsFile } from "nodefony";
2
+ import { findProjectRoot, isDocVisible, packageManagerCommandLines, packageManagerExecArgs, portableSpawn, readDocScope, readSymbolsGraph, resolvePackageManager } from "nodefony";
3
3
  import { existsSync } from "node:fs";
4
4
  import { promisify } from "node:util";
5
5
  import { readFile, readdir, stat } from "node:fs/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFile, spawn } from "node:child_process";
8
- //#region nodefony/src/docsReader.ts
8
+ // nodefony/src/docsReader.ts
9
9
  const execFileAsync = promisify(execFile);
10
10
  /**
11
11
  * Racine de l'APPLICATION servie — jamais le dossier courant du process.
@@ -97,11 +97,19 @@ async function gitLastUpdated(file) {
97
97
  return null;
98
98
  }
99
99
  }
100
- /** Construit un `DocSummary` à partir du contenu brut d'un `.md`. */
101
- async function summarize(docsDir, fileName, withGit) {
100
+ /**
101
+ * La page se sert-elle dans ce décor ? Portée lue dans `publish` par la règle
102
+ * du cœur (`readDocScope`/`isDocVisible`) — la même que le portail et le site.
103
+ */
104
+ function servable(data, frameworkRepository) {
105
+ return isDocVisible(readDocScope(data.publish), frameworkRepository);
106
+ }
107
+ /** Construit un `DocSummary` à partir du contenu brut d'un `.md` — `null` si la page n'est pas servie ici. */
108
+ async function summarize(docsDir, fileName, withGit, frameworkRepository) {
102
109
  const slug = basename(fileName, extname(fileName));
103
110
  const full = join(docsDir, fileName);
104
111
  const { data, body } = parseFrontmatter(await readFile(full, "utf8"));
112
+ if (!servable(data, frameworkRepository)) return null;
105
113
  const title = docTitle(data, body, slug);
106
114
  return {
107
115
  slug,
@@ -121,7 +129,7 @@ async function summarize(docsDir, fileName, withGit) {
121
129
  * @param modulePath - chemin disque du module (`Module.path`).
122
130
  * @returns liste triée (vide si le dossier `docs/` est absent).
123
131
  */
124
- async function listModuleDocs(modulePath, withGit = false) {
132
+ async function listModuleDocs(modulePath, withGit = false, frameworkRepository = false) {
125
133
  const docsDir = join(modulePath, "docs");
126
134
  let entries;
127
135
  try {
@@ -130,21 +138,34 @@ async function listModuleDocs(modulePath, withGit = false) {
130
138
  return [];
131
139
  }
132
140
  const mdFiles = entries.filter((f) => f.toLowerCase().endsWith(".md"));
133
- return (await Promise.all(mdFiles.map((f) => summarize(docsDir, f, withGit)))).sort((a, b) => a.order - b.order || a.slug.localeCompare(b.slug));
141
+ return (await Promise.all(mdFiles.map((f) => summarize(docsDir, f, withGit, frameworkRepository)))).filter((d) => d !== null).sort((a, b) => a.order - b.order || a.slug.localeCompare(b.slug));
134
142
  }
135
143
  /**
136
- * Comptage rapide des docs (`*.md`) d'un module — readdir seul, sans lire/parser
137
- * ni `git`. Pour les KPI/overview (`docsCount`) où seul le nombre importe.
144
+ * Comptage des docs SERVIES d'un module, sans `git` — pour les KPI/overview
145
+ * (`docsCount`) où seul le nombre importe. Le frontmatter est lu : le compte
146
+ * suit la même portée (`publish`) que la liste, sinon le KPI annonce des pages
147
+ * que l'onglet Docs refuse d'ouvrir.
138
148
  *
139
149
  * @param modulePath - chemin disque du module (`Module.path`).
140
- * @returns nombre de fichiers `.md` dans `<modulePath>/docs/` (0 si absent).
150
+ * @param frameworkRepository - vrai dans le dépôt du framework (pages `core` servies).
151
+ * @returns nombre de pages servies dans `<modulePath>/docs/` (0 si absent).
141
152
  */
142
- async function countModuleDocs(modulePath) {
153
+ async function countModuleDocs(modulePath, frameworkRepository = false) {
154
+ const docsDir = join(modulePath, "docs");
155
+ let entries;
143
156
  try {
144
- return (await readdir(join(modulePath, "docs"))).filter((f) => f.toLowerCase().endsWith(".md")).length;
157
+ entries = await readdir(docsDir);
145
158
  } catch {
146
159
  return 0;
147
160
  }
161
+ return (await Promise.all(entries.filter((f) => f.toLowerCase().endsWith(".md")).map(async (f) => {
162
+ try {
163
+ const { data } = parseFrontmatter(await readFile(join(docsDir, f), "utf8"));
164
+ return servable(data, frameworkRepository);
165
+ } catch {
166
+ return false;
167
+ }
168
+ }))).filter(Boolean).length;
148
169
  }
149
170
  /**
150
171
  * Lit une doc module par slug : frontmatter + corps markdown brut.
@@ -154,7 +175,7 @@ async function countModuleDocs(modulePath) {
154
175
  *
155
176
  * @returns `null` si slug invalide ou fichier absent.
156
177
  */
157
- async function readModuleDoc(modulePath, slug) {
178
+ async function readModuleDoc(modulePath, slug, frameworkRepository = false) {
158
179
  if (!SLUG_RE.test(slug)) return null;
159
180
  const full = join(modulePath, "docs", `${slug}.md`);
160
181
  let raw;
@@ -164,6 +185,7 @@ async function readModuleDoc(modulePath, slug) {
164
185
  return null;
165
186
  }
166
187
  const { data, body } = parseFrontmatter(raw);
188
+ if (!servable(data, frameworkRepository)) return null;
167
189
  return {
168
190
  slug,
169
191
  frontmatter: data,
@@ -276,9 +298,10 @@ async function searchModuleDocs(targets, query, options = {}) {
276
298
  } catch {
277
299
  continue;
278
300
  }
279
- scanned += 1;
280
301
  const slug = basename(fileName, extname(fileName));
281
302
  const { data, body } = parseFrontmatter(raw);
303
+ if (!servable(data, options.frameworkRepository ?? false)) continue;
304
+ scanned += 1;
282
305
  const title = docTitle(data, body, slug);
283
306
  const foldedTitle = fold(`${title} ${typeof data.navTitle === "string" && data.navTitle.trim() ? data.navTitle.trim() : ""} ${slug}`);
284
307
  const lines = body.split("\n");
@@ -347,15 +370,9 @@ async function searchModuleDocs(targets, query, options = {}) {
347
370
  * @returns symboles exportés triés par kind puis nom (vide si fichier absent).
348
371
  */
349
372
  async function listModuleSymbols(packageName) {
350
- const file = resolveSymbolsFile(process.cwd());
351
- if (file === null) return [];
352
- let parsed;
353
- try {
354
- parsed = JSON.parse(await readFile(file, "utf8"));
355
- } catch {
356
- return [];
357
- }
358
- const symbols = parsed.symbols ?? {};
373
+ const graph = readSymbolsGraph(process.cwd());
374
+ if (graph === null) return [];
375
+ const symbols = graph.symbols;
359
376
  const out = [];
360
377
  for (const sym of Object.values(symbols)) {
361
378
  if (sym.module !== packageName || sym.exported !== true) continue;
@@ -935,5 +952,4 @@ async function readCoreInfo() {
935
952
  dependencies
936
953
  };
937
954
  }
938
- //#endregion
939
955
  export { CORE_PACKAGE, checkOutdated, countModuleDocs, listModuleDocs, listModuleSymbols, listTestFiles, listTestGroups, parseFrontmatter, readCoreInfo, readCoverage, readDependencies, readModuleDoc, readSymbolDeclaration, resolveCorePath, runModuleTests, searchModuleDocs, testRunCommand };
@@ -1,6 +1,6 @@
1
1
  import { RequestContext } from "nodefony";
2
2
  import { createHash } from "node:crypto";
3
- //#region nodefony/src/idempotency.ts
3
+ // nodefony/src/idempotency.ts
4
4
  /**
5
5
  * Logique **pure et partagée** de la porte d'idempotence des mutations, conforme à
6
6
  * `draft-ietf-httpapi-idempotency-key-header-06`. Source unique de la sémantique
@@ -51,28 +51,21 @@ function resolveIdempotencyKey(alsKey, header) {
51
51
  }
52
52
  /**
53
53
  * Identité stable pour **scoper** le cache d'idempotence (anti-IDOR : un
54
- * utilisateur ne doit jamais rejouer la clé d'un autre). Dérivée de l'utilisateur
55
- * (`username` / `identifier` / `id`, sans coupler le framework au contrat `IUser`),
56
- * avec fallback sur l'`userId` de l'ALS. `null` = pas d'identité fiable → l'appelant
57
- * n'utilise PAS le cache (jamais de partage cross-identité).
54
+ * utilisateur ne doit jamais rejouer la clé d'un autre) : l'`identifier` de
55
+ * l'utilisateur AUTHENTIFIÉ de la requête. `null` = pas d'identité fiable →
56
+ * l'appelant n'utilise PAS le cache (jamais de partage cross-identité).
58
57
  *
59
- * ⚠️ Doit renvoyer la MÊME valeur quel que soit le transport pour un même compte
60
- * (sinon une mutation tentée en WS puis rejouée en HTTP ne dédoublonnerait pas).
61
- * D'où la dérivation de `request.user` (posé UNIFORMÉMENT dans l'ALS par les deux
62
- * transports), et NON de `getUserId()` seul (le firewall HTTP ne le pose pas
63
- * toujours — vécu S4).
58
+ * La décision « authentifié ? » est celle du cœur
59
+ * (`RequestContext.getAuthenticatedUser`) : l'anonyme d'une zone `anonymous`
60
+ * rend `null`. Lire l'`identifier` de l'utilisateur BRUT faisait partager à
61
+ * tous les visiteurs anonymes la même identité (`anon.`), donc le même cache.
62
+ *
63
+ * Même valeur quel que soit le transport pour un même compte : le pare-feu HTTP
64
+ * et le pont realtime posent tous deux le jeton et l'utilisateur dans l'ALS —
65
+ * une mutation tentée en WS puis rejouée en HTTP se dédoublonne.
64
66
  */
65
- function resolveIdentity(user) {
66
- if (user && typeof user === "object") {
67
- const o = user;
68
- for (const v of [
69
- o.username,
70
- o.identifier,
71
- o.id
72
- ]) if (typeof v === "string" && v) return v;
73
- }
74
- const uid = RequestContext.getUserId();
75
- return typeof uid === "string" && uid ? uid : null;
67
+ function resolveIdentity() {
68
+ return RequestContext.getAuthenticatedUser()?.identifier ?? null;
76
69
  }
77
70
  /**
78
71
  * Empreinte SHA-256 du **payload** d'une requête (parties sérialisables : nom de
@@ -133,5 +126,4 @@ async function evaluateIdempotency(opts) {
133
126
  };
134
127
  }
135
128
  }
136
- //#endregion
137
129
  export { IDEMPOTENCY_KEY_MAX, computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity };
@@ -1,5 +1,5 @@
1
1
  import { GcScheduler } from "nodefony";
2
- //#region nodefony/src/idempotencyGc.ts
2
+ // nodefony/src/idempotencyGc.ts
3
3
  /**
4
4
  * Arme un {@link GcScheduler} qui purge périodiquement les entrées expirées d'un
5
5
  * store d'idempotence — **UNIQUEMENT si le store expose `gc()`**.
@@ -28,5 +28,4 @@ function scheduleIdempotencyGc(store, opts) {
28
28
  opts.log?.(armed ? `idempotency gc armed — purge every ${opts.intervalS}s (SQL store exposes gc())` : `idempotency gc disarmed — intervalS=${opts.intervalS} (delegated to cron / external)`);
29
29
  return scheduler;
30
30
  }
31
- //#endregion
32
31
  export { scheduleIdempotencyGc };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/idempotencyStoreRegistry.ts
1
+ // nodefony/src/idempotencyStoreRegistry.ts
2
2
  const factories = /* @__PURE__ */ new Map();
3
3
  /**
4
4
  * Enregistre (ou remplace) la fabrique d'un store d'idempotence distribué.
@@ -32,5 +32,4 @@ function listIdempotencyStores() {
32
32
  function listIdempotencyBackends() {
33
33
  return ["memory", ...factories.keys()];
34
34
  }
35
- //#endregion
36
35
  export { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencyStores, registerIdempotencyStore };