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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +4 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +7 -6
  5. package/dist/nodefony/controller/AdminApiController.js +4 -3
  6. package/dist/nodefony/controller/ApiKeyController.js +1 -1
  7. package/dist/nodefony/controller/OAuth2Controller.js +11 -6
  8. package/dist/nodefony/controller/SessionAuthController.js +3 -3
  9. package/dist/nodefony/controller/TokenAuthController.js +2 -2
  10. package/dist/nodefony/controller/TotpController.js +5 -4
  11. package/dist/nodefony/controller/WebAuthnController.js +4 -4
  12. package/dist/nodefony/decorators/routerDecorators.js +41 -35
  13. package/dist/nodefony/service/AdminBroker.js +2 -2
  14. package/dist/nodefony/service/IdempotencyStore.js +4 -5
  15. package/dist/nodefony/service/router.js +75 -25
  16. package/dist/nodefony/src/Controller.js +184 -78
  17. package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
  18. package/dist/nodefony/src/KernelAdminApi.js +60 -55
  19. package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
  20. package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
  21. package/dist/nodefony/src/Resolver.js +219 -63
  22. package/dist/nodefony/src/Route.js +38 -38
  23. package/dist/nodefony/src/SyslogAdminApi.js +4 -4
  24. package/dist/nodefony/src/docsReader.js +22 -18
  25. package/dist/nodefony/src/scopeCatalog.js +1 -1
  26. package/dist/nodefony/src/singletonGuard.js +76 -0
  27. package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
  28. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
  29. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
  30. package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
  31. package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
  32. package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
  33. package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
  34. package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
  35. package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
  36. package/dist/types/nodefony/service/router.d.ts +33 -7
  37. package/dist/types/nodefony/src/Controller.d.ts +97 -24
  38. package/dist/types/nodefony/src/Resolver.d.ts +154 -15
  39. package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
  40. package/dist/types/nodefony/src/Route.d.ts +24 -22
  41. package/dist/types/nodefony/src/docsReader.d.ts +3 -3
  42. package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
  43. package/docs/admin.md +26 -26
  44. package/docs/controller.md +139 -121
  45. package/docs/decorateurs.md +108 -105
  46. package/docs/idempotence.md +78 -71
  47. package/docs/routing.md +57 -51
  48. package/docs/templates.md +17 -17
  49. package/package.json +12 -12
@@ -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, 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
@@ -50,11 +50,11 @@ function readAdapterManifest(pkg) {
50
50
  const pkgJson = join(REPO_ROOT, "node_modules", ...pkg.split("/"), "package.json");
51
51
  try {
52
52
  if (!existsSync(pkgJson)) return null;
53
- const nf = JSON.parse(readFileSync(pkgJson, "utf8")).nodefony;
54
- if (!nf || !Array.isArray(nf.stores)) return null;
53
+ const manifest = parseStoreManifest(JSON.parse(readFileSync(pkgJson, "utf8")));
54
+ if (!manifest?.provides) return null;
55
55
  return {
56
- storeKind: nf.storeKind === "cache" ? "cache" : "durable",
57
- stores: nf.stores.filter((s) => typeof s === "string")
56
+ storeKind: manifest.storeKind,
57
+ stores: manifest.stores
58
58
  };
59
59
  } catch {
60
60
  return null;
@@ -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,
@@ -692,7 +685,12 @@ function createKernelAdminApi(kernel) {
692
685
  if ((res.resolved === "drizzle" || res.resolved === "mongoose") && infra.database) return "NF_DATABASE_URL";
693
686
  return null;
694
687
  };
695
- const registered = new Set(kernel.storeResolutions.flatMap((r) => [...r.available]));
688
+ const loadedPackages = /* @__PURE__ */ new Set();
689
+ for (const mod of Object.values(kernel.modules)) {
690
+ const npmName = mod.package?.name;
691
+ if (typeof npmName === "string") loadedPackages.add(npmName);
692
+ loadedPackages.add(mod.name);
693
+ }
696
694
  return {
697
695
  engines: OFFICIAL_STORE_ADAPTERS.map((a) => {
698
696
  const manifest = readAdapterManifest(a.package);
@@ -703,7 +701,7 @@ function createKernelAdminApi(kernel) {
703
701
  kind: manifest?.storeKind ?? "durable",
704
702
  provides: manifest?.stores ?? [],
705
703
  installed: manifest !== null || isPackageInstalled(a.package),
706
- loaded: registered.has(a.engine)
704
+ loaded: loadedPackages.has(a.package) || loadedPackages.has(a.engine)
707
705
  };
708
706
  }),
709
707
  infra: {
@@ -745,7 +743,7 @@ function createKernelAdminApi(kernel) {
745
743
  reason: "prod_immutable"
746
744
  }
747
745
  };
748
- const key = request.params.module;
746
+ const key = request.params.module ?? "";
749
747
  const mod = kernel.getModules()[key];
750
748
  if (!mod) return {
751
749
  status: 404,
@@ -806,17 +804,24 @@ function createKernelAdminApi(kernel) {
806
804
  };
807
805
  const opts = mod.options ?? {};
808
806
  const before = getResolvedPath(opts, segments);
809
- if (!applyResolvedPath(opts, segments, value)) return {
807
+ const nextOpts = withResolvedPath(opts, segments, value);
808
+ if (nextOpts === null) return {
810
809
  status: 422,
811
810
  body: {
812
811
  error: "Path not present in module config",
813
812
  path: body.path
814
813
  }
815
814
  };
815
+ const moduleInstance = kernel.getModules()[key];
816
+ if (moduleInstance) kernel.replaceModuleOptions(moduleInstance, nextOpts);
816
817
  const svcMod = mod;
817
818
  for (const sname of svcMod.getServiceNames?.() ?? []) {
818
819
  const svc = svcMod.get?.(sname);
819
- 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
+ }
820
825
  svc?.onConfigChanged?.(segments);
821
826
  }
822
827
  let edited = runtimeEdited.get(key);
@@ -909,7 +914,7 @@ function createKernelAdminApi(kernel) {
909
914
  path: "module/{name}",
910
915
  summary: "Detail of one module by key (http, framework, … or core)",
911
916
  handler: async (request) => {
912
- const key = request.params.name;
917
+ const key = request.params.name ?? "";
913
918
  if (key === CORE_KEY) {
914
919
  const core = await readCoreInfo();
915
920
  return {
@@ -944,10 +949,10 @@ function createKernelAdminApi(kernel) {
944
949
  return {
945
950
  key,
946
951
  name: cfg.name,
947
- version: mod.getModuleVersion?.() ?? null,
952
+ version: mod.getModuleVersion() ?? null,
948
953
  isApp: cfg.isApp,
949
954
  path: relPath(mod.path),
950
- dependencies: mod.getDependencies?.() ?? [],
955
+ dependencies: mod.getDependencies(),
951
956
  services,
952
957
  config: cfg.config,
953
958
  configSchema: cfg.configSchema,
@@ -964,7 +969,7 @@ function createKernelAdminApi(kernel) {
964
969
  path: "module/{name}/dependencies",
965
970
  summary: "Module dependencies with installed versions",
966
971
  handler: async (request) => {
967
- const target = resolveTarget(request.params.name);
972
+ const target = resolveTarget(request.params.name ?? "");
968
973
  if (!target) return {
969
974
  status: 404,
970
975
  body: {
@@ -983,7 +988,7 @@ function createKernelAdminApi(kernel) {
983
988
  path: "module/{name}/dependencies/outdated",
984
989
  summary: "Check external dependencies for updates (npm registry)",
985
990
  handler: async (request) => {
986
- const target = resolveTarget(request.params.name);
991
+ const target = resolveTarget(request.params.name ?? "");
987
992
  if (!target) return {
988
993
  status: 404,
989
994
  body: {
@@ -1003,7 +1008,7 @@ function createKernelAdminApi(kernel) {
1003
1008
  path: "module/{name}/docs",
1004
1009
  summary: "Documentation index of one module (markdown in <module>/docs)",
1005
1010
  handler: async (request) => {
1006
- const key = request.params.name;
1011
+ const key = request.params.name ?? "";
1007
1012
  const dir = resolveDocDir(key);
1008
1013
  if (!dir) return {
1009
1014
  status: 404,
@@ -1023,7 +1028,7 @@ function createKernelAdminApi(kernel) {
1023
1028
  path: "module/{name}/docs/{slug}",
1024
1029
  summary: "Raw markdown of one module doc by slug",
1025
1030
  handler: async (request) => {
1026
- const key = request.params.name;
1031
+ const key = request.params.name ?? "";
1027
1032
  const dir = resolveDocDir(key);
1028
1033
  if (!dir) return {
1029
1034
  status: 404,
@@ -1033,7 +1038,7 @@ function createKernelAdminApi(kernel) {
1033
1038
  available: docKeys()
1034
1039
  }
1035
1040
  };
1036
- const doc = await readModuleDoc(dir, request.params.slug);
1041
+ const doc = await readModuleDoc(dir, request.params.slug ?? "");
1037
1042
  if (!doc) return {
1038
1043
  status: 404,
1039
1044
  body: {
@@ -1062,7 +1067,7 @@ function createKernelAdminApi(kernel) {
1062
1067
  markdown: section.markdown
1063
1068
  };
1064
1069
  }
1065
- if (request.query.outline !== void 0) {
1070
+ if (Object.hasOwn(request.query, "outline")) {
1066
1071
  const { markdown, ...rest } = doc;
1067
1072
  return {
1068
1073
  ...rest,
@@ -1113,7 +1118,7 @@ function createKernelAdminApi(kernel) {
1113
1118
  path: "module/{name}/symbol/{symbol}",
1114
1119
  summary: "Declaration (signature + TSDoc) of one exported symbol",
1115
1120
  handler: async (request) => {
1116
- const key = request.params.name;
1121
+ const key = request.params.name ?? "";
1117
1122
  const modulePath = resolveTarget(key)?.path ?? resolvePackageDir(key);
1118
1123
  if (!modulePath) return {
1119
1124
  status: 404,
@@ -1123,7 +1128,7 @@ function createKernelAdminApi(kernel) {
1123
1128
  available: docKeys()
1124
1129
  }
1125
1130
  };
1126
- const symbol = request.params.symbol;
1131
+ const symbol = request.params.symbol ?? "";
1127
1132
  const found = await readSymbolDeclaration(modulePath, symbol);
1128
1133
  if (!found) return {
1129
1134
  status: 404,
@@ -1145,7 +1150,7 @@ function createKernelAdminApi(kernel) {
1145
1150
  path: "module/{name}/symbols",
1146
1151
  summary: "Exported TS symbols + TSDoc descriptions (.ai/symbols.json)",
1147
1152
  handler: async (request) => {
1148
- const key = request.params.name;
1153
+ const key = request.params.name ?? "";
1149
1154
  const target = resolveTarget(key);
1150
1155
  if (!target) return {
1151
1156
  status: 404,
@@ -1166,7 +1171,7 @@ function createKernelAdminApi(kernel) {
1166
1171
  path: "module/{name}/coverage",
1167
1172
  summary: "Latest test coverage report (vitest json-summary)",
1168
1173
  handler: async (request) => {
1169
- const key = request.params.name;
1174
+ const key = request.params.name ?? "";
1170
1175
  const target = resolveTarget(key);
1171
1176
  if (!target) return {
1172
1177
  status: 404,
@@ -1186,7 +1191,7 @@ function createKernelAdminApi(kernel) {
1186
1191
  path: "module/{name}/tests",
1187
1192
  summary: "List test files of one module",
1188
1193
  handler: async (request) => {
1189
- const key = request.params.name;
1194
+ const key = request.params.name ?? "";
1190
1195
  const target = resolveTarget(key);
1191
1196
  if (!target) return {
1192
1197
  status: 404,
@@ -1213,7 +1218,7 @@ function createKernelAdminApi(kernel) {
1213
1218
  status: 403,
1214
1219
  body: { error: "Test runner disabled outside development" }
1215
1220
  };
1216
- const key = request.params.name;
1221
+ const key = request.params.name ?? "";
1217
1222
  const target = resolveTarget(key);
1218
1223
  if (!target) return {
1219
1224
  status: 404,
@@ -1241,7 +1246,7 @@ function createKernelAdminApi(kernel) {
1241
1246
  startedAt: Date.now()
1242
1247
  });
1243
1248
  if (testJobs.size > 16) {
1244
- 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);
1245
1250
  if (oldest) testJobs.delete(oldest[0]);
1246
1251
  }
1247
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
  }