@nodefony/framework 10.0.0-alpha.9 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +2 -2
  3. package/dist/nodefony/controller/AdminApiController.js +4 -3
  4. package/dist/nodefony/controller/ApiKeyController.js +1 -1
  5. package/dist/nodefony/controller/OAuth2Controller.js +11 -6
  6. package/dist/nodefony/controller/SessionAuthController.js +3 -3
  7. package/dist/nodefony/controller/TokenAuthController.js +2 -2
  8. package/dist/nodefony/controller/TotpController.js +5 -4
  9. package/dist/nodefony/controller/WebAuthnController.js +4 -4
  10. package/dist/nodefony/decorators/routerDecorators.js +41 -35
  11. package/dist/nodefony/service/IdempotencyStore.js +2 -3
  12. package/dist/nodefony/service/router.js +73 -23
  13. package/dist/nodefony/src/Controller.js +184 -78
  14. package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
  15. package/dist/nodefony/src/KernelAdminApi.js +49 -49
  16. package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
  17. package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
  18. package/dist/nodefony/src/Resolver.js +219 -63
  19. package/dist/nodefony/src/Route.js +38 -38
  20. package/dist/nodefony/src/SyslogAdminApi.js +4 -4
  21. package/dist/nodefony/src/docsReader.js +22 -18
  22. package/dist/nodefony/src/scopeCatalog.js +1 -1
  23. package/dist/nodefony/src/singletonGuard.js +76 -0
  24. package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
  25. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
  26. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
  27. package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
  28. package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
  29. package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
  30. package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
  31. package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
  32. package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
  33. package/dist/types/nodefony/service/router.d.ts +33 -7
  34. package/dist/types/nodefony/src/Controller.d.ts +97 -24
  35. package/dist/types/nodefony/src/Resolver.d.ts +154 -15
  36. package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
  37. package/dist/types/nodefony/src/Route.d.ts +24 -22
  38. package/dist/types/nodefony/src/docsReader.d.ts +3 -3
  39. package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
  40. package/docs/admin.md +1 -1
  41. package/docs/controller.md +133 -115
  42. package/docs/decorateurs.md +95 -92
  43. package/docs/idempotence.md +21 -19
  44. package/docs/routing.md +46 -40
  45. package/docs/templates.md +14 -14
  46. package/package.json +8 -8
@@ -2,7 +2,7 @@ import { CORE_PACKAGE, checkOutdated, countModuleDocs, listModuleDocs, listModul
2
2
  import { getResolvedPath, navigateSchemaNode, nodeFlags, notEditableReason, recipeFor, validateLeafValue } from "./configMutation.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { join } from "node:path";
5
- import { GitService, Syslog, aggregateProvenance, applyResolvedPath, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath } from "nodefony";
5
+ import { GitService, Syslog, aggregateProvenance, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath, withResolvedPath } from "nodefony";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  //#region nodefony/src/KernelAdminApi.ts
@@ -153,9 +153,8 @@ function flattenPaths(value, prefix, out) {
153
153
  function attributeOverrideSources(entries) {
154
154
  const byTarget = /* @__PURE__ */ new Map();
155
155
  for (const e of entries) for (const key of Object.keys(e.config)) {
156
- const m = /^module-(.+)$/i.exec(key);
157
- if (!m) continue;
158
- const targetSeg = m[1].toLowerCase();
156
+ const targetSeg = /^module-(.+)$/i.exec(key)?.[1]?.toLowerCase();
157
+ if (targetSeg === void 0) continue;
159
158
  const paths = /* @__PURE__ */ new Set();
160
159
  flattenPaths(e.config[key], "", paths);
161
160
  const arr = byTarget.get(targetSeg);
@@ -214,7 +213,7 @@ function computeConfigEntry(key, mod, runtimePaths) {
214
213
  const envPaths = new Set(parseNfEnvOverrides(process.env).filter((o) => o.moduleSeg === seg).map((o) => o.path.join(".")));
215
214
  provenance = computeConfigProvenance(opts, extractJsonSchemaDefaults(schema), envPaths);
216
215
  }
217
- if (provenance && runtimePaths && runtimePaths.size) {
216
+ if (provenance && runtimePaths?.size) {
218
217
  for (const k of Object.keys(provenance)) if (runtimePaths.has(k.toLowerCase())) provenance[k] = "runtime";
219
218
  }
220
219
  return {
@@ -259,12 +258,13 @@ function auditConfigChange(kernel, request, moduleKey, path, before, after) {
259
258
  });
260
259
  }
261
260
  /**
262
- * Journalise un changement de debug runtime (catégorie `log`). Même découplage
261
+ * Journalise un changement de debug runtime (catégorie `config` : une verbosité
262
+ * est une configuration runtime). Même découplage
263
263
  * que {@link auditConfigChange} (résolution par nom, no-op si pas d'audit).
264
264
  */
265
265
  function auditLogLevelChange(kernel, request, module, action, level, ttlMs) {
266
266
  (kernel.container?.get("auditService"))?.record?.({
267
- category: "log",
267
+ category: "config",
268
268
  action: `log.debug.${action}`,
269
269
  outcome: "success",
270
270
  actor: actorLabel(request.user),
@@ -352,7 +352,7 @@ function createKernelAdminApi(kernel) {
352
352
  if (!mod) return null;
353
353
  return {
354
354
  path: mod.path,
355
- pkg: mod.getModuleName?.() ?? key
355
+ pkg: mod.getModuleName() ?? key
356
356
  };
357
357
  };
358
358
  const resolvePackageDir = (name) => {
@@ -401,7 +401,7 @@ function createKernelAdminApi(kernel) {
401
401
  const runtimeEdited = /* @__PURE__ */ new Map();
402
402
  const devGuard = () => kernel.environment === "development" || Boolean(kernel.debug);
403
403
  const repoRoot = process.cwd();
404
- const relPath = (p) => p && p.startsWith(repoRoot) ? p.slice(repoRoot.length).replace(/^[/\\]+/, "") || "." : p ?? null;
404
+ const relPath = (p) => p?.startsWith(repoRoot) ? p.slice(repoRoot.length).replace(/^[/\\]+/, "") || "." : p ?? null;
405
405
  const redactInfraUrl = (url) => url.replace(/\/\/[^/@]*@/, "//***@");
406
406
  const buildConfigEntries = () => {
407
407
  const modules = kernel.getModules();
@@ -581,16 +581,13 @@ function createKernelAdminApi(kernel) {
581
581
  isApp: false,
582
582
  path: relPath(core.path)
583
583
  }];
584
- for (const name of Object.keys(modules)) {
585
- const mod = modules[name];
586
- list.push({
587
- key: name,
588
- name: mod.getModuleName?.() ?? name,
589
- version: mod.getModuleVersion?.() ?? null,
590
- isApp: mod.isApp ?? false,
591
- path: relPath(mod.path)
592
- });
593
- }
584
+ for (const [name, mod] of Object.entries(modules)) list.push({
585
+ key: name,
586
+ name: mod.getModuleName() ?? name,
587
+ version: mod.getModuleVersion() ?? null,
588
+ isApp: mod.isApp,
589
+ path: relPath(mod.path)
590
+ });
594
591
  return list;
595
592
  }
596
593
  },
@@ -600,14 +597,11 @@ function createKernelAdminApi(kernel) {
600
597
  handler: () => {
601
598
  const modules = kernel.getModules();
602
599
  const services = [];
603
- for (const name of Object.keys(modules)) {
604
- const mod = modules[name];
605
- for (const service of mod.getServiceNames?.() ?? []) services.push({
606
- name: service,
607
- module: name,
608
- class: mod.get(service)?.constructor?.name ?? null
609
- });
610
- }
600
+ for (const [name, mod] of Object.entries(modules)) for (const service of mod.getServiceNames()) services.push({
601
+ name: service,
602
+ module: name,
603
+ class: mod.get(service)?.constructor?.name ?? null
604
+ });
611
605
  services.sort((a, b) => `${a.module}.${a.name}`.localeCompare(`${b.module}.${b.name}`));
612
606
  return services;
613
607
  }
@@ -627,8 +621,7 @@ function createKernelAdminApi(kernel) {
627
621
  const entries = buildConfigEntries();
628
622
  const matches = (name, key) => name.toLowerCase() === filter || key.toLowerCase() === filter;
629
623
  if (filter !== null) {
630
- const entry = entries.find((e) => matches(e.name, e.key));
631
- if (!entry || entry.configSchema == null) {
624
+ if (entries.find((e) => matches(e.name, e.key))?.configSchema == null) {
632
625
  const modules = kernel.getModules();
633
626
  const nameOf = (k) => modules[k].getModuleName?.() ?? k;
634
627
  const loadedKey = Object.keys(modules).find((k) => matches(nameOf(k), k));
@@ -658,7 +651,7 @@ function createKernelAdminApi(kernel) {
658
651
  if (filter !== null && entry.name.toLowerCase() !== filter && entry.key.toLowerCase() !== filter) continue;
659
652
  for (const leaf of flattenConfigSchema(entry.configSchema)) {
660
653
  const path = leaf.key.split(".");
661
- const note = SECRET_KEY.test(path[path.length - 1]) && !leaf.note.includes("secret") ? [leaf.note, "secret"].filter(Boolean).join(", ") : leaf.note;
654
+ const note = SECRET_KEY.test(path.at(-1) ?? "") && !leaf.note.includes("secret") ? [leaf.note, "secret"].filter(Boolean).join(", ") : leaf.note;
662
655
  rows.push({
663
656
  key: leaf.key,
664
657
  module: entry.name,
@@ -750,7 +743,7 @@ function createKernelAdminApi(kernel) {
750
743
  reason: "prod_immutable"
751
744
  }
752
745
  };
753
- const key = request.params.module;
746
+ const key = request.params.module ?? "";
754
747
  const mod = kernel.getModules()[key];
755
748
  if (!mod) return {
756
749
  status: 404,
@@ -811,17 +804,24 @@ function createKernelAdminApi(kernel) {
811
804
  };
812
805
  const opts = mod.options ?? {};
813
806
  const before = getResolvedPath(opts, segments);
814
- if (!applyResolvedPath(opts, segments, value)) return {
807
+ const nextOpts = withResolvedPath(opts, segments, value);
808
+ if (nextOpts === null) return {
815
809
  status: 422,
816
810
  body: {
817
811
  error: "Path not present in module config",
818
812
  path: body.path
819
813
  }
820
814
  };
815
+ const moduleInstance = kernel.getModules()[key];
816
+ if (moduleInstance) kernel.replaceModuleOptions(moduleInstance, nextOpts);
821
817
  const svcMod = mod;
822
818
  for (const sname of svcMod.getServiceNames?.() ?? []) {
823
819
  const svc = svcMod.get?.(sname);
824
- if (svc?.options && svc.options !== opts) applyResolvedPath(svc.options, segments, value);
820
+ if (svc?.options === opts) svc.options = nextOpts;
821
+ else if (svc?.options) {
822
+ const nextSvc = withResolvedPath(svc.options, segments, value);
823
+ if (nextSvc !== null) svc.options = nextSvc;
824
+ }
825
825
  svc?.onConfigChanged?.(segments);
826
826
  }
827
827
  let edited = runtimeEdited.get(key);
@@ -914,7 +914,7 @@ function createKernelAdminApi(kernel) {
914
914
  path: "module/{name}",
915
915
  summary: "Detail of one module by key (http, framework, … or core)",
916
916
  handler: async (request) => {
917
- const key = request.params.name;
917
+ const key = request.params.name ?? "";
918
918
  if (key === CORE_KEY) {
919
919
  const core = await readCoreInfo();
920
920
  return {
@@ -949,10 +949,10 @@ function createKernelAdminApi(kernel) {
949
949
  return {
950
950
  key,
951
951
  name: cfg.name,
952
- version: mod.getModuleVersion?.() ?? null,
952
+ version: mod.getModuleVersion() ?? null,
953
953
  isApp: cfg.isApp,
954
954
  path: relPath(mod.path),
955
- dependencies: mod.getDependencies?.() ?? [],
955
+ dependencies: mod.getDependencies(),
956
956
  services,
957
957
  config: cfg.config,
958
958
  configSchema: cfg.configSchema,
@@ -969,7 +969,7 @@ function createKernelAdminApi(kernel) {
969
969
  path: "module/{name}/dependencies",
970
970
  summary: "Module dependencies with installed versions",
971
971
  handler: async (request) => {
972
- const target = resolveTarget(request.params.name);
972
+ const target = resolveTarget(request.params.name ?? "");
973
973
  if (!target) return {
974
974
  status: 404,
975
975
  body: {
@@ -988,7 +988,7 @@ function createKernelAdminApi(kernel) {
988
988
  path: "module/{name}/dependencies/outdated",
989
989
  summary: "Check external dependencies for updates (npm registry)",
990
990
  handler: async (request) => {
991
- const target = resolveTarget(request.params.name);
991
+ const target = resolveTarget(request.params.name ?? "");
992
992
  if (!target) return {
993
993
  status: 404,
994
994
  body: {
@@ -1008,7 +1008,7 @@ function createKernelAdminApi(kernel) {
1008
1008
  path: "module/{name}/docs",
1009
1009
  summary: "Documentation index of one module (markdown in <module>/docs)",
1010
1010
  handler: async (request) => {
1011
- const key = request.params.name;
1011
+ const key = request.params.name ?? "";
1012
1012
  const dir = resolveDocDir(key);
1013
1013
  if (!dir) return {
1014
1014
  status: 404,
@@ -1028,7 +1028,7 @@ function createKernelAdminApi(kernel) {
1028
1028
  path: "module/{name}/docs/{slug}",
1029
1029
  summary: "Raw markdown of one module doc by slug",
1030
1030
  handler: async (request) => {
1031
- const key = request.params.name;
1031
+ const key = request.params.name ?? "";
1032
1032
  const dir = resolveDocDir(key);
1033
1033
  if (!dir) return {
1034
1034
  status: 404,
@@ -1038,7 +1038,7 @@ function createKernelAdminApi(kernel) {
1038
1038
  available: docKeys()
1039
1039
  }
1040
1040
  };
1041
- const doc = await readModuleDoc(dir, request.params.slug);
1041
+ const doc = await readModuleDoc(dir, request.params.slug ?? "");
1042
1042
  if (!doc) return {
1043
1043
  status: 404,
1044
1044
  body: {
@@ -1067,7 +1067,7 @@ function createKernelAdminApi(kernel) {
1067
1067
  markdown: section.markdown
1068
1068
  };
1069
1069
  }
1070
- if (request.query.outline !== void 0) {
1070
+ if (Object.hasOwn(request.query, "outline")) {
1071
1071
  const { markdown, ...rest } = doc;
1072
1072
  return {
1073
1073
  ...rest,
@@ -1118,7 +1118,7 @@ function createKernelAdminApi(kernel) {
1118
1118
  path: "module/{name}/symbol/{symbol}",
1119
1119
  summary: "Declaration (signature + TSDoc) of one exported symbol",
1120
1120
  handler: async (request) => {
1121
- const key = request.params.name;
1121
+ const key = request.params.name ?? "";
1122
1122
  const modulePath = resolveTarget(key)?.path ?? resolvePackageDir(key);
1123
1123
  if (!modulePath) return {
1124
1124
  status: 404,
@@ -1128,7 +1128,7 @@ function createKernelAdminApi(kernel) {
1128
1128
  available: docKeys()
1129
1129
  }
1130
1130
  };
1131
- const symbol = request.params.symbol;
1131
+ const symbol = request.params.symbol ?? "";
1132
1132
  const found = await readSymbolDeclaration(modulePath, symbol);
1133
1133
  if (!found) return {
1134
1134
  status: 404,
@@ -1150,7 +1150,7 @@ function createKernelAdminApi(kernel) {
1150
1150
  path: "module/{name}/symbols",
1151
1151
  summary: "Exported TS symbols + TSDoc descriptions (.ai/symbols.json)",
1152
1152
  handler: async (request) => {
1153
- const key = request.params.name;
1153
+ const key = request.params.name ?? "";
1154
1154
  const target = resolveTarget(key);
1155
1155
  if (!target) return {
1156
1156
  status: 404,
@@ -1171,7 +1171,7 @@ function createKernelAdminApi(kernel) {
1171
1171
  path: "module/{name}/coverage",
1172
1172
  summary: "Latest test coverage report (vitest json-summary)",
1173
1173
  handler: async (request) => {
1174
- const key = request.params.name;
1174
+ const key = request.params.name ?? "";
1175
1175
  const target = resolveTarget(key);
1176
1176
  if (!target) return {
1177
1177
  status: 404,
@@ -1191,7 +1191,7 @@ function createKernelAdminApi(kernel) {
1191
1191
  path: "module/{name}/tests",
1192
1192
  summary: "List test files of one module",
1193
1193
  handler: async (request) => {
1194
- const key = request.params.name;
1194
+ const key = request.params.name ?? "";
1195
1195
  const target = resolveTarget(key);
1196
1196
  if (!target) return {
1197
1197
  status: 404,
@@ -1218,7 +1218,7 @@ function createKernelAdminApi(kernel) {
1218
1218
  status: 403,
1219
1219
  body: { error: "Test runner disabled outside development" }
1220
1220
  };
1221
- const key = request.params.name;
1221
+ const key = request.params.name ?? "";
1222
1222
  const target = resolveTarget(key);
1223
1223
  if (!target) return {
1224
1224
  status: 404,
@@ -1246,7 +1246,7 @@ function createKernelAdminApi(kernel) {
1246
1246
  startedAt: Date.now()
1247
1247
  });
1248
1248
  if (testJobs.size > 16) {
1249
- const oldest = [...testJobs.entries()].sort((a, b) => a[1].startedAt - b[1].startedAt)[0];
1249
+ const oldest = [...testJobs.entries()].sort((a, b) => a[1].startedAt - b[1].startedAt).at(0);
1250
1250
  if (oldest) testJobs.delete(oldest[0]);
1251
1251
  }
1252
1252
  runModuleTests(target.path, file).then((result) => testJobs.set(jobId, {
@@ -4,8 +4,8 @@ import AdminApiController from "../controller/AdminApiController.js";
4
4
  //#region nodefony/src/PlaygroundAdminApi.ts
5
5
  /** Normalise `requirements.methods` (string | string[]) → tableau majuscule. */
6
6
  function methodsOf(route) {
7
- const m = route.requirements?.methods ?? route.method;
8
- if (Array.isArray(m)) return m.map((x) => String(x).toUpperCase());
7
+ const m = route.requirements.methods ?? route.method;
8
+ if (Array.isArray(m)) return m.map((x) => x.toUpperCase());
9
9
  if (typeof m === "string") return m.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
10
10
  return ["ANY"];
11
11
  }
@@ -44,7 +44,7 @@ function serializeAction(route) {
44
44
  csrfProtect: meta.csrfProtect,
45
45
  csrfExempt: meta.csrfExempt,
46
46
  session: meta.sessionIntent,
47
- bypassFirewall: route.bypassFirewall === true
47
+ bypassFirewall: route.bypassFirewall
48
48
  }
49
49
  };
50
50
  }
@@ -173,7 +173,7 @@ var RedisIdempotencyStore = class {
173
173
  }
174
174
  /** Préfixe effectif des clés, cloisonné par application (mémoïsé). */
175
175
  #prefix() {
176
- if (this.#prefixCache === null) this.#prefixCache = this.#resolvePrefix();
176
+ this.#prefixCache ??= this.#resolvePrefix();
177
177
  return this.#prefixCache;
178
178
  }
179
179
  #key(key) {
@@ -227,7 +227,7 @@ var RedisIdempotencyStore = class {
227
227
  if (!client) return;
228
228
  const k = this.#key(key);
229
229
  const existing = this.#parse(await client.get(k));
230
- if (existing === null || existing.s !== "if") {
230
+ if (existing?.s !== "if") {
231
231
  this.#dec();
232
232
  return;
233
233
  }