@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.
- package/README.md +4 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/controller/AdminApiController.js +4 -3
- package/dist/nodefony/controller/ApiKeyController.js +1 -1
- package/dist/nodefony/controller/OAuth2Controller.js +11 -6
- package/dist/nodefony/controller/SessionAuthController.js +3 -3
- package/dist/nodefony/controller/TokenAuthController.js +2 -2
- package/dist/nodefony/controller/TotpController.js +5 -4
- package/dist/nodefony/controller/WebAuthnController.js +4 -4
- package/dist/nodefony/decorators/routerDecorators.js +41 -35
- package/dist/nodefony/service/IdempotencyStore.js +2 -3
- package/dist/nodefony/service/router.js +73 -23
- package/dist/nodefony/src/Controller.js +184 -78
- package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
- package/dist/nodefony/src/KernelAdminApi.js +49 -49
- package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
- package/dist/nodefony/src/Resolver.js +219 -63
- package/dist/nodefony/src/Route.js +38 -38
- package/dist/nodefony/src/SyslogAdminApi.js +4 -4
- package/dist/nodefony/src/docsReader.js +22 -18
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/singletonGuard.js +76 -0
- package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
- package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
- package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
- package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
- package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
- package/dist/types/nodefony/service/router.d.ts +33 -7
- package/dist/types/nodefony/src/Controller.d.ts +97 -24
- package/dist/types/nodefony/src/Resolver.d.ts +154 -15
- package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
- package/dist/types/nodefony/src/Route.d.ts +24 -22
- package/dist/types/nodefony/src/docsReader.d.ts +3 -3
- package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
- package/docs/admin.md +1 -1
- package/docs/controller.md +133 -115
- package/docs/decorateurs.md +95 -92
- package/docs/idempotence.md +21 -19
- package/docs/routing.md +46 -40
- package/docs/templates.md +14 -14
- 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,
|
|
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
|
|
157
|
-
if (
|
|
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
|
|
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 `
|
|
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: "
|
|
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
|
|
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
|
|
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.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
8
|
-
if (Array.isArray(m)) return m.map((x) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
230
|
+
if (existing?.s !== "if") {
|
|
231
231
|
this.#dec();
|
|
232
232
|
return;
|
|
233
233
|
}
|