@nodefony/framework 10.0.0-beta.3 → 10.0.0-beta.5
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 +19 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +9 -18
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +39 -8
- package/dist/nodefony/controller/ApiKeyController.js +16 -8
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +4 -11
- package/dist/nodefony/controller/OAuth2Controller.js +3 -4
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +13 -4
- package/dist/nodefony/controller/TokenAuthController.js +3 -4
- package/dist/nodefony/controller/TotpController.js +2 -3
- package/dist/nodefony/controller/WebAuthnController.js +2 -3
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +155 -53
- package/dist/nodefony/service/AdminBroker.js +4 -6
- package/dist/nodefony/service/Eta.js +1 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -6
- package/dist/nodefony/service/router.js +49 -15
- package/dist/nodefony/src/Controller.js +32 -39
- package/dist/nodefony/src/FrameworkAdminApi.js +31 -10
- package/dist/nodefony/src/KernelAdminApi.js +7 -9
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +68 -33
- package/dist/nodefony/src/ResourceController.js +13 -6
- package/dist/nodefony/src/Route.js +2 -3
- package/dist/nodefony/src/SecurityApiController.js +1 -2
- package/dist/nodefony/src/SyslogAdminApi.js +1 -2
- package/dist/nodefony/src/Template.js +1 -2
- package/dist/nodefony/src/configMutation.js +1 -2
- package/dist/nodefony/src/docsReader.js +1 -2
- package/dist/nodefony/src/idempotency.js +1 -2
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +2 -3
- package/dist/nodefony/src/requireAuthentication.js +1 -2
- package/dist/nodefony/src/routeSchema.js +81 -0
- package/dist/nodefony/src/scopeCatalog.js +1 -2
- package/dist/nodefony/src/singletonGuard.js +1 -2
- package/dist/nodefony/src/syslogFilters.js +7 -2
- package/dist/types/index.d.ts +5 -10
- package/dist/types/nodefony/controller/AdminApiController.d.ts +13 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +7 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +133 -32
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/service/router.d.ts +27 -1
- package/dist/types/nodefony/src/Controller.d.ts +23 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +33 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +12 -4
- package/dist/types/nodefony/src/Route.d.ts +3 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/dist/types/nodefony/src/syslogFilters.d.ts +6 -0
- package/docs/admin.md +131 -82
- package/docs/controller.md +109 -110
- package/docs/decorateurs.md +214 -161
- package/docs/idempotence.md +154 -136
- package/docs/routing.md +105 -90
- package/docs/templates.md +55 -55
- package/package.json +8 -11
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import router_default from "../service/router.js";
|
|
2
2
|
import { createPlaygroundEndpoints } from "./PlaygroundAdminApi.js";
|
|
3
|
-
import { parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
-
|
|
3
|
+
import { PageQueryError, parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
+
// nodefony/src/FrameworkAdminApi.ts
|
|
5
5
|
/**
|
|
6
6
|
* Capacités de page d'un endpoint, ÉVALUÉES sous filet.
|
|
7
7
|
*
|
|
@@ -29,6 +29,34 @@ function readPageCapabilities(caps) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
+
* Lit le paramètre `filters` de `routes/page` : un tableau JSON de
|
|
33
|
+
* `{ key, op, value }`. Absent → aucun filtre.
|
|
34
|
+
*
|
|
35
|
+
* @throws PageQueryError (400) si le JSON est cassé ou d'une autre forme — un
|
|
36
|
+
* filtre ignoré rendrait TOUTES les routes, lues comme son résultat.
|
|
37
|
+
*/
|
|
38
|
+
function parseRouteFilters(raw) {
|
|
39
|
+
if (!raw) return [];
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new PageQueryError(`Invalid "filters": not JSON.`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(parsed)) throw new PageQueryError(`Invalid "filters": expected a JSON array.`);
|
|
47
|
+
const entries = parsed;
|
|
48
|
+
const filters = [];
|
|
49
|
+
for (const item of entries) {
|
|
50
|
+
if (typeof item !== "object" || item === null || !("key" in item) || !("op" in item) || typeof item.key !== "string" || typeof item.op !== "string") throw new PageQueryError(`Invalid "filters": each entry needs a string "key" and "op".`);
|
|
51
|
+
filters.push({
|
|
52
|
+
key: item.key,
|
|
53
|
+
op: item.op,
|
|
54
|
+
value: "value" in item ? item.value : void 0
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return filters;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
32
60
|
* Producteur `IAdminApi` du module **framework** — exposé sous
|
|
33
61
|
* `/nodefony/framework/api/*`.
|
|
34
62
|
*
|
|
@@ -205,13 +233,7 @@ function createFrameworkAdminApi(broker, opts) {
|
|
|
205
233
|
});
|
|
206
234
|
parseFilters(request.query, {}, { accepts: ["filters"] });
|
|
207
235
|
const search = query.q?.toLowerCase() ?? "";
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const raw = one(request.query.filters);
|
|
211
|
-
if (raw) filters = JSON.parse(raw);
|
|
212
|
-
} catch {
|
|
213
|
-
filters = [];
|
|
214
|
-
}
|
|
236
|
+
const filters = parseRouteFilters(one(request.query.filters));
|
|
215
237
|
let rows = router_default.routes.map(serializeRoute);
|
|
216
238
|
if (search) rows = rows.filter((r) => matchesSearch(r, search));
|
|
217
239
|
for (const f of filters) rows = rows.filter((r) => matchOp(cell(r, f.key), f.op, f.value));
|
|
@@ -297,5 +319,4 @@ function createFrameworkAdminApi(broker, opts) {
|
|
|
297
319
|
adminEndpoints: () => endpoints
|
|
298
320
|
};
|
|
299
321
|
}
|
|
300
|
-
//#endregion
|
|
301
322
|
export { createFrameworkAdminApi };
|
|
@@ -2,10 +2,10 @@ 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, collectDevStatus, collectEnvShortcuts, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, isFrameworkRepository, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath, resumeSupervisor, runAppContributions, suspendSupervisor, withResolvedPath } from "nodefony";
|
|
5
|
+
import { GitService, Syslog, aggregateProvenance, collectDevStatus, collectEnvShortcuts, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, isFrameworkRepository, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath, redactUrlCredentials, resumeSupervisor, runAppContributions, suspendSupervisor, withResolvedPath } from "nodefony";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
-
|
|
8
|
+
// nodefony/src/KernelAdminApi.ts
|
|
9
9
|
/** Clé du pseudo-module core dans Studio (cf carte "Core" / `resolveCorePath`). */
|
|
10
10
|
const CORE_KEY = "core";
|
|
11
11
|
/** Racine projet — pour ne PAS exposer de chemin absolu (sécu). */
|
|
@@ -109,7 +109,7 @@ const SECRET_KEY = /(?:^|[a-z0-9])(secret|password|passwd|passphrase|credential|
|
|
|
109
109
|
function safeConfig(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
110
110
|
if (typeof value === "function") return "[Function]";
|
|
111
111
|
if (typeof value === "bigint") return value.toString();
|
|
112
|
-
if (typeof value === "string") return stripAbs(value);
|
|
112
|
+
if (typeof value === "string") return stripAbs(redactUrlCredentials(value));
|
|
113
113
|
if (value === null || typeof value !== "object") return value;
|
|
114
114
|
if (depth > 5) return "[depth limit]";
|
|
115
115
|
if (seen.has(value)) return "[Circular]";
|
|
@@ -441,7 +441,6 @@ function createKernelAdminApi(kernel) {
|
|
|
441
441
|
const devGuard = () => kernel.environment === "development" || Boolean(kernel.debug);
|
|
442
442
|
const repoRoot = process.cwd();
|
|
443
443
|
const relPath = (p) => p?.startsWith(repoRoot) ? p.slice(repoRoot.length).replace(/^[/\\]+/, "") || "." : p ?? null;
|
|
444
|
-
const redactInfraUrl = (url) => url.replace(/\/\/[^/@]*@/, "//***@");
|
|
445
444
|
const buildConfigEntries = () => {
|
|
446
445
|
const modules = kernel.getModules();
|
|
447
446
|
const entries = [];
|
|
@@ -736,8 +735,8 @@ function createKernelAdminApi(kernel) {
|
|
|
736
735
|
handler: () => {
|
|
737
736
|
const infra = kernel.infra;
|
|
738
737
|
const entriesBySeg = new Map(buildConfigEntries().map((e) => [e.seg, e]));
|
|
739
|
-
const dbUrl = infra.database ?
|
|
740
|
-
const cacheUrl = infra.cache ?
|
|
738
|
+
const dbUrl = infra.database ? redactUrlCredentials(infra.database.url) : null;
|
|
739
|
+
const cacheUrl = infra.cache ? redactUrlCredentials(infra.cache.url) : null;
|
|
741
740
|
const storeEndpoint = (res) => {
|
|
742
741
|
if (res.location) return void 0;
|
|
743
742
|
if (res.resolved === "redis") return cacheUrl ?? void 0;
|
|
@@ -776,8 +775,8 @@ function createKernelAdminApi(kernel) {
|
|
|
776
775
|
} : null,
|
|
777
776
|
cache: infra.cache ? { url: cacheUrl } : null,
|
|
778
777
|
logs: infra.logs ? {
|
|
779
|
-
lokiUrl: infra.logs.lokiUrl ?
|
|
780
|
-
opensearchUrl: infra.logs.opensearchUrl ?
|
|
778
|
+
lokiUrl: infra.logs.lokiUrl ? redactUrlCredentials(infra.logs.lokiUrl) : null,
|
|
779
|
+
opensearchUrl: infra.logs.opensearchUrl ? redactUrlCredentials(infra.logs.opensearchUrl) : null
|
|
781
780
|
} : null
|
|
782
781
|
},
|
|
783
782
|
stores: kernel.storeResolutions.map((res) => {
|
|
@@ -1367,5 +1366,4 @@ function createKernelAdminApi(kernel) {
|
|
|
1367
1366
|
adminEndpoints: () => endpoints
|
|
1368
1367
|
};
|
|
1369
1368
|
}
|
|
1370
|
-
//#endregion
|
|
1371
1369
|
export { createKernelAdminApi, nodefonyPackageCandidates, safeConfig };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { extractActionScopes, resolveActionMeta } from "../decorators/routerDecorators.js";
|
|
2
2
|
import router_default from "../service/router.js";
|
|
3
3
|
import AdminApiController from "../controller/AdminApiController.js";
|
|
4
|
-
|
|
4
|
+
import { standardJsonSchemaOf } from "nodefony";
|
|
5
|
+
// nodefony/src/PlaygroundAdminApi.ts
|
|
5
6
|
/** Normalise `requirements.methods` (string | string[]) → tableau majuscule. */
|
|
6
7
|
function methodsOf(route) {
|
|
7
8
|
const m = route.requirements.methods ?? route.method;
|
|
@@ -15,11 +16,79 @@ function publicDefaults(route) {
|
|
|
15
16
|
for (const [k, v] of Object.entries(route.defaults)) if (k !== "controller") out[k] = v;
|
|
16
17
|
return out;
|
|
17
18
|
}
|
|
19
|
+
/** Source du formulaire pour chaque partie d'un schéma de route. */
|
|
20
|
+
const PART_SOURCE = {
|
|
21
|
+
params: "param",
|
|
22
|
+
query: "query",
|
|
23
|
+
body: "body",
|
|
24
|
+
headers: "headers"
|
|
25
|
+
};
|
|
26
|
+
/** JSON Schema d'entrée d'un schéma, ou `null` s'il ne sait pas se décrire. */
|
|
27
|
+
function describeSchema(schema) {
|
|
28
|
+
const converter = standardJsonSchemaOf(schema);
|
|
29
|
+
if (converter === null) return null;
|
|
30
|
+
try {
|
|
31
|
+
return converter.input({ target: "draft-2020-12" });
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Les champs de premier niveau d'un JSON Schema objet. */
|
|
37
|
+
function propertiesOf(json) {
|
|
38
|
+
const properties = json?.properties;
|
|
39
|
+
return typeof properties === "object" && properties !== null ? Object.keys(properties) : [];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Les entrées d'une action à schéma, au format des paramètres décorés, et le
|
|
43
|
+
* contrat décrit partie par partie.
|
|
44
|
+
*/
|
|
45
|
+
function schemaParams(schema, bodyStream) {
|
|
46
|
+
const params = [];
|
|
47
|
+
const described = {};
|
|
48
|
+
for (const part of [
|
|
49
|
+
"params",
|
|
50
|
+
"query",
|
|
51
|
+
"body",
|
|
52
|
+
"headers"
|
|
53
|
+
]) {
|
|
54
|
+
const partSchema = schema[part];
|
|
55
|
+
if (partSchema === void 0) continue;
|
|
56
|
+
const json = describeSchema(partSchema);
|
|
57
|
+
described[part] = json;
|
|
58
|
+
const source = PART_SOURCE[part];
|
|
59
|
+
if (part === "body") {
|
|
60
|
+
params.push({
|
|
61
|
+
source,
|
|
62
|
+
key: null,
|
|
63
|
+
index: 0,
|
|
64
|
+
stream: false
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const key of propertiesOf(json)) params.push({
|
|
69
|
+
source,
|
|
70
|
+
key,
|
|
71
|
+
index: 0,
|
|
72
|
+
stream: false
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (bodyStream) params.push({
|
|
76
|
+
source: "body",
|
|
77
|
+
key: null,
|
|
78
|
+
index: 0,
|
|
79
|
+
stream: true
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
params,
|
|
83
|
+
described
|
|
84
|
+
};
|
|
85
|
+
}
|
|
18
86
|
/** Sérialise une route en action playground (métadonnées figées, JSON-safe). */
|
|
19
87
|
function serializeAction(route) {
|
|
20
88
|
const meta = resolveActionMeta(route);
|
|
21
89
|
const methods = methodsOf(route);
|
|
22
90
|
const scopes = route.controller && route.classMethod ? extractActionScopes(route.controller, route.classMethod) : [];
|
|
91
|
+
const fromSchema = meta.schema !== null ? schemaParams(meta.schema, meta.bodyStream) : null;
|
|
23
92
|
return {
|
|
24
93
|
route: route.name,
|
|
25
94
|
path: route.path ?? null,
|
|
@@ -28,12 +97,13 @@ function serializeAction(route) {
|
|
|
28
97
|
action: route.classMethod ?? null,
|
|
29
98
|
variables: [...route.variables],
|
|
30
99
|
defaults: publicDefaults(route),
|
|
31
|
-
params: meta.paramsMeta?.map((p) => ({
|
|
100
|
+
params: fromSchema?.params ?? meta.paramsMeta?.map((p) => ({
|
|
32
101
|
source: p.source,
|
|
33
102
|
key: p.key ?? null,
|
|
34
103
|
index: p.index,
|
|
35
104
|
stream: p.stream === true
|
|
36
105
|
})).sort((a, b) => a.index - b.index) ?? [],
|
|
106
|
+
schema: fromSchema?.described ?? null,
|
|
37
107
|
guards: {
|
|
38
108
|
security: meta.security ? { clauses: meta.security.clauses.map((c) => ({
|
|
39
109
|
anyOf: [...c.anyOf],
|
|
@@ -93,5 +163,4 @@ function createPlaygroundEndpoints() {
|
|
|
93
163
|
handler: () => buildPlaygroundSnapshot()
|
|
94
164
|
}];
|
|
95
165
|
}
|
|
96
|
-
//#endregion
|
|
97
166
|
export { buildPlaygroundSnapshot, createPlaygroundEndpoints };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertPageQuery } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/RedisIdempotencyStore.ts
|
|
3
3
|
/** Préfixe namespacé des clés d'idempotence dans Redis. */
|
|
4
4
|
/**
|
|
5
5
|
* Préfixe HISTORIQUE des clés d'idempotence. Utilisé tel quel par une application
|
|
@@ -42,6 +42,50 @@ function decodeCursor(cursor) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
+
* Reconstruit une entrée relue du store, ou `null` si sa forme est inattendue.
|
|
46
|
+
* La valeur vient de Redis, pas de ce processus : une entrée « terminée » sans
|
|
47
|
+
* réponse rejouerait `undefined` comme une réponse HTTP.
|
|
48
|
+
*/
|
|
49
|
+
function toEntry(v) {
|
|
50
|
+
if (typeof v !== "object" || v === null || !("s" in v) || !("f" in v)) return null;
|
|
51
|
+
const f = v.f;
|
|
52
|
+
if (typeof f !== "string") return null;
|
|
53
|
+
if (v.s === "if") return {
|
|
54
|
+
s: "if",
|
|
55
|
+
f
|
|
56
|
+
};
|
|
57
|
+
if (v.s !== "d" || !("r" in v)) return null;
|
|
58
|
+
const r = toResponse(v.r);
|
|
59
|
+
return r === null ? null : {
|
|
60
|
+
s: "d",
|
|
61
|
+
f,
|
|
62
|
+
r
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Reconstruit une réponse mémorisée, ou `null` si sa forme est inattendue. */
|
|
66
|
+
function toResponse(v) {
|
|
67
|
+
if (typeof v !== "object" || v === null || !("status" in v)) return null;
|
|
68
|
+
const status = v.status;
|
|
69
|
+
if (typeof status !== "number") return null;
|
|
70
|
+
const body = "body" in v ? v.body : void 0;
|
|
71
|
+
if (!("headers" in v) || v.headers === void 0) return {
|
|
72
|
+
status,
|
|
73
|
+
body
|
|
74
|
+
};
|
|
75
|
+
const headers = v.headers;
|
|
76
|
+
if (typeof headers !== "object" || headers === null) return null;
|
|
77
|
+
const copy = {};
|
|
78
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
79
|
+
if (typeof value !== "string") return null;
|
|
80
|
+
copy[name] = value;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
headers: copy,
|
|
85
|
+
body
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
45
89
|
* Store d'idempotence **Redis** (node-redis v6) — implémentation distribuée
|
|
46
90
|
* d'{@link IIdempotencyStore} pour le cluster (dédup des mutations rejouées
|
|
47
91
|
* PARTAGÉE cross-pod, là où l'impl mémoire par défaut reste affine à un pod).
|
|
@@ -183,8 +227,7 @@ var RedisIdempotencyStore = class {
|
|
|
183
227
|
#parse(raw) {
|
|
184
228
|
if (raw === null) return null;
|
|
185
229
|
try {
|
|
186
|
-
|
|
187
|
-
return v && (v.s === "if" || v.s === "d") ? v : null;
|
|
230
|
+
return toEntry(JSON.parse(raw));
|
|
188
231
|
} catch {
|
|
189
232
|
return null;
|
|
190
233
|
}
|
|
@@ -250,5 +293,4 @@ var RedisIdempotencyStore = class {
|
|
|
250
293
|
if (this.#pending > 0) this.#pending--;
|
|
251
294
|
}
|
|
252
295
|
};
|
|
253
|
-
//#endregion
|
|
254
296
|
export { RedisIdempotencyStore };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { guardSingletonState, isDevelopment } from "./singletonGuard.js";
|
|
2
|
-
import {
|
|
2
|
+
import { validateRouteInput } from "./routeSchema.js";
|
|
3
|
+
import { buildParamArgs, computeActionMeta, readInvocationBody, readRouteInput, resolveActionMeta, resolveSessionIntent } from "../decorators/routerDecorators.js";
|
|
3
4
|
import { computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity } from "./idempotency.js";
|
|
4
5
|
import { authenticationShortfall, stepUpChallenge } from "./requireAuthentication.js";
|
|
5
6
|
import { RequestContext, finallyMaybe, identityHint, isArray, isPlainObject, isPromise, nodefonyError, thenMaybe, typeOf } from "nodefony";
|
|
6
7
|
import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
7
|
-
|
|
8
|
+
// nodefony/src/Resolver.ts
|
|
8
9
|
/**
|
|
9
10
|
* Résout une route vers son couple controller/action et exécute l'action —
|
|
10
11
|
* UN Resolver est alloué par requête HTTP (et par connexion WS, réutilisé
|
|
@@ -47,7 +48,7 @@ var Resolver = class {
|
|
|
47
48
|
/**
|
|
48
49
|
* Query d'une invocation **par message** (pont WS-RPC `api.request`) — pendant
|
|
49
50
|
* de `cleanPathOverride` pour le `?…` du path invoqué. Le contexte WS étant
|
|
50
|
-
* PARTAGÉ par la connexion (sa `
|
|
51
|
+
* PARTAGÉ par la connexion (sa `query` = celle du handshake), la query
|
|
51
52
|
* per-invocation vit ici (le Resolver est per-invocation → zéro bleed entre
|
|
52
53
|
* requêtes concurrentes d'une même socket). `null` (hot path HTTP) = ignoré.
|
|
53
54
|
* Consommé par `@Query` (`_buildParamArgs`) et copié sur le controller
|
|
@@ -75,6 +76,11 @@ var Resolver = class {
|
|
|
75
76
|
* administrateurs.
|
|
76
77
|
*/
|
|
77
78
|
messageInvocation = false;
|
|
79
|
+
/**
|
|
80
|
+
* Les gardes de l'action ont été jugées favorables par {@link authorizeAction}
|
|
81
|
+
* et ne se rejugent pas à la prochaine exécution — consommé par elle.
|
|
82
|
+
*/
|
|
83
|
+
preAuthorized = false;
|
|
78
84
|
constructor(context) {
|
|
79
85
|
this.context = context;
|
|
80
86
|
this.injector = context.container?.get("injector") ?? null;
|
|
@@ -84,7 +90,7 @@ var Resolver = class {
|
|
|
84
90
|
if (match) {
|
|
85
91
|
this.variables = match;
|
|
86
92
|
this.route = route;
|
|
87
|
-
this.controller = route.controller;
|
|
93
|
+
this.controller = route.controller ?? null;
|
|
88
94
|
this.actionName = route.classMethod;
|
|
89
95
|
this.resolve = true;
|
|
90
96
|
this.bypassFirewall = this.route.bypassFirewall;
|
|
@@ -231,7 +237,7 @@ var Resolver = class {
|
|
|
231
237
|
* @returns le controller reçu, pour enchaîner.
|
|
232
238
|
*/
|
|
233
239
|
_guardSingleton(controller, ctx) {
|
|
234
|
-
if (this.controller
|
|
240
|
+
if (this.controller?.scope === "singleton" && isDevelopment(ctx.kernel?.environment)) guardSingletonState(controller, controller.constructor.name);
|
|
235
241
|
return controller;
|
|
236
242
|
}
|
|
237
243
|
/**
|
|
@@ -256,9 +262,46 @@ var Resolver = class {
|
|
|
256
262
|
*/
|
|
257
263
|
executeAction(data, reload = false, metaArg) {
|
|
258
264
|
const meta = metaArg ?? (this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName));
|
|
265
|
+
let guarded;
|
|
266
|
+
if (this.preAuthorized) this.preAuthorized = false;
|
|
267
|
+
else guarded = this._enforceActionSecurity(meta);
|
|
268
|
+
const schema = meta.schema;
|
|
269
|
+
if (schema !== null) {
|
|
270
|
+
if (guarded !== void 0) return guarded.then(() => this._validateAndInvoke(schema, meta, data, reload));
|
|
271
|
+
return this._validateAndInvoke(schema, meta, data, reload);
|
|
272
|
+
}
|
|
273
|
+
if (guarded !== void 0) return guarded.then(() => this._acquireAndInvoke(meta, data, reload, null));
|
|
274
|
+
return this._acquireAndInvoke(meta, data, reload, null);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Valide l'entrée d'une action à schéma, puis l'exécute avec cette entrée
|
|
278
|
+
* pour argument unique. Synchrone tant que le validateur l'est.
|
|
279
|
+
*
|
|
280
|
+
* @throws SchemaValidationError (422) quand l'entrée viole le schéma.
|
|
281
|
+
*/
|
|
282
|
+
_validateAndInvoke(schema, meta, data, reload) {
|
|
283
|
+
const validated = validateRouteInput(schema, readRouteInput(this._zipRouteParams(), this.context.request, this.queryOverride ?? void 0, meta.bodyStream));
|
|
284
|
+
if (validated instanceof Promise) return validated.then((input) => this._acquireAndInvoke(meta, data, reload, input));
|
|
285
|
+
return this._acquireAndInvoke(meta, data, reload, validated);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Juge les gardes de l'action résolue SANS l'exécuter — appelé par le
|
|
289
|
+
* pipeline WebSocket AVANT d'accepter le handshake (#596), pour qu'un
|
|
290
|
+
* connecté sans le rôle reçoive son 403 en statut HTTP au lieu d'une
|
|
291
|
+
* ouverture suivie d'une fermeture. Un verdict favorable vaut pour la
|
|
292
|
+
* PROCHAINE exécution (l'action de handshake), qui ne le rejuge pas ; les
|
|
293
|
+
* messages suivants, eux, sont jugés chacun.
|
|
294
|
+
*
|
|
295
|
+
* @returns `undefined` quand l'action n'est pas gardée ; sinon la promesse du
|
|
296
|
+
* verdict, rompue par un refus
|
|
297
|
+
*/
|
|
298
|
+
authorizeAction() {
|
|
299
|
+
const meta = this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName);
|
|
259
300
|
const guarded = this._enforceActionSecurity(meta);
|
|
260
|
-
if (guarded
|
|
261
|
-
return
|
|
301
|
+
if (guarded === void 0) return void 0;
|
|
302
|
+
return guarded.then(() => {
|
|
303
|
+
this.preAuthorized = true;
|
|
304
|
+
});
|
|
262
305
|
}
|
|
263
306
|
/**
|
|
264
307
|
* Les gardes d'autorisation d'une action : `@IsGranted`/`@RequireScope`, puis
|
|
@@ -296,10 +339,10 @@ var Resolver = class {
|
|
|
296
339
|
* c'est la bonne classe, sinon une création —, puis l'action elle-même.
|
|
297
340
|
* N'attend que la création, et seulement quand elle attend.
|
|
298
341
|
*/
|
|
299
|
-
_acquireAndInvoke(meta, data, reload) {
|
|
342
|
+
_acquireAndInvoke(meta, data, reload, input) {
|
|
300
343
|
const pinned = this.context.container?.get("controller");
|
|
301
|
-
if (!pinned || reload || this.controller && !(pinned instanceof this.controller)) return thenMaybe(this.newController(), (controller) => this._invokeAction(controller, meta, data));
|
|
302
|
-
return this._invokeAction(pinned, meta, data);
|
|
344
|
+
if (!pinned || reload || this.controller && !(pinned instanceof this.controller)) return thenMaybe(this.newController(), (controller) => this._invokeAction(controller, meta, data, input));
|
|
345
|
+
return this._invokeAction(pinned, meta, data, input);
|
|
303
346
|
}
|
|
304
347
|
/**
|
|
305
348
|
* Appelle l'action sur l'instance : état de requête (portée `request`
|
|
@@ -308,17 +351,15 @@ var Resolver = class {
|
|
|
308
351
|
*
|
|
309
352
|
* @throws de façon SYNCHRONE ce que lève l'action, ou l'absence d'action.
|
|
310
353
|
*/
|
|
311
|
-
_invokeAction(controller, meta, data) {
|
|
354
|
+
_invokeAction(controller, meta, data, input) {
|
|
312
355
|
if (this.controller?.scope !== "singleton") {
|
|
313
356
|
controller.setRoute(this.route);
|
|
314
|
-
if (this.queryOverride !== null)
|
|
315
|
-
controller.queryGet = this.queryOverride;
|
|
316
|
-
controller.query = this.queryOverride;
|
|
317
|
-
}
|
|
357
|
+
if (this.queryOverride !== null) controller.query = this.queryOverride;
|
|
318
358
|
}
|
|
319
359
|
const methodKey = this.actionName;
|
|
320
360
|
let args;
|
|
321
|
-
if (
|
|
361
|
+
if (input !== null) args = [input];
|
|
362
|
+
else if (meta.paramsMeta) args = this._buildParamArgs(meta.paramsMeta);
|
|
322
363
|
else if (data) args = [...this.variables, ...data];
|
|
323
364
|
else args = [...this.variables];
|
|
324
365
|
this._applyResponseMeta(meta);
|
|
@@ -393,7 +434,7 @@ var Resolver = class {
|
|
|
393
434
|
const als = RequestContext.get();
|
|
394
435
|
const httpReq = context.request;
|
|
395
436
|
const params = this._zipRouteParams();
|
|
396
|
-
const body =
|
|
437
|
+
const body = readInvocationBody(httpReq) ?? null;
|
|
397
438
|
const store = context.container?.get("idempotencyStore");
|
|
398
439
|
const verdict = await evaluateIdempotency({
|
|
399
440
|
store,
|
|
@@ -404,7 +445,7 @@ var Resolver = class {
|
|
|
404
445
|
params,
|
|
405
446
|
body
|
|
406
447
|
]),
|
|
407
|
-
isWs:
|
|
448
|
+
isWs: context.type === "websocket",
|
|
408
449
|
required: meta.idempotent?.required ?? true
|
|
409
450
|
});
|
|
410
451
|
if (verdict.kind === "reject") throw new nodefonyError(verdict.message, verdict.status);
|
|
@@ -538,16 +579,14 @@ var Resolver = class {
|
|
|
538
579
|
return idx === -1 ? void 0 : this.variables[idx];
|
|
539
580
|
}
|
|
540
581
|
_buildParamArgs(metas) {
|
|
541
|
-
const
|
|
542
|
-
const paramsMap = this._zipRouteParams();
|
|
543
|
-
const ctx = this.context;
|
|
582
|
+
const context = this.context;
|
|
544
583
|
return buildParamArgs(metas, {
|
|
545
|
-
paramsMap,
|
|
546
|
-
request:
|
|
547
|
-
response:
|
|
548
|
-
session:
|
|
584
|
+
paramsMap: this._zipRouteParams(),
|
|
585
|
+
request: context.request,
|
|
586
|
+
response: context.response,
|
|
587
|
+
session: context.session,
|
|
549
588
|
queryOverride: this.queryOverride ?? void 0,
|
|
550
|
-
getRequestCookies: (name) =>
|
|
589
|
+
getRequestCookies: (name) => context.getRequestCookies(name)
|
|
551
590
|
});
|
|
552
591
|
}
|
|
553
592
|
/**
|
|
@@ -630,20 +669,17 @@ var Resolver = class {
|
|
|
630
669
|
}
|
|
631
670
|
switch (this.context.type) {
|
|
632
671
|
case "http":
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
case "https": this.context.waitAsync = true;
|
|
672
|
+
this.context.waitAsync = true;
|
|
673
|
+
this.unrendered = `une instance de ${(typeof result === "object" && result !== null ? result.constructor.name : void 0) ?? "classe"}`;
|
|
636
674
|
}
|
|
637
675
|
return;
|
|
638
676
|
default: switch (this.context.type) {
|
|
639
677
|
case "http":
|
|
640
|
-
case "http2":
|
|
641
|
-
case "http3":
|
|
642
|
-
case "https":
|
|
643
678
|
if (this.context.sended) return;
|
|
644
679
|
if (this.context.isRedirect) return this.context.send();
|
|
645
680
|
if (NO_BODY_STATUS.has(this.getResponseStatus())) return this.context.send();
|
|
646
681
|
this.context.waitAsync = true;
|
|
682
|
+
this.unrendered = result === null ? "null" : typeof result;
|
|
647
683
|
}
|
|
648
684
|
}
|
|
649
685
|
}
|
|
@@ -683,5 +719,4 @@ const NO_BODY_STATUS = /* @__PURE__ */ new Set([
|
|
|
683
719
|
205,
|
|
684
720
|
304
|
|
685
721
|
]);
|
|
686
|
-
//#endregion
|
|
687
722
|
export { Resolver as default };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Controller from "./Controller.js";
|
|
2
2
|
import { assertPageQuery } from "nodefony";
|
|
3
3
|
import { HttpError } from "@nodefony/http";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/ResourceController.ts
|
|
5
5
|
/**
|
|
6
|
-
* Controller de ressource **
|
|
6
|
+
* Controller de ressource **duplex** (V4.2 — une action, deux transports) :
|
|
7
7
|
* la logique métier est écrite UNE fois dans le service de ressource, les
|
|
8
8
|
* actions de la sous-classe ne sont que des portes (REST, WS-RPC `invoke`,
|
|
9
9
|
* GraphQL à venir) qui retournent la **valeur brute** — `returnController`
|
|
@@ -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.
|
|
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
|
*
|
|
@@ -38,8 +38,12 @@ import { HttpError } from "@nodefony/http";
|
|
|
38
38
|
* return this.listResource();
|
|
39
39
|
* }
|
|
40
40
|
* \@route("books-get", { path: "/{id}", requirements: { methods: ["GET", "WEBSOCKET"] } })
|
|
41
|
-
* detail(\@Param("id") id: string) {
|
|
42
|
-
*
|
|
41
|
+
* async detail(\@Param("id") id: string) {
|
|
42
|
+
* // `null` rendu tel quel = « l'action répond elle-même » : la requête
|
|
43
|
+
* // pendrait jusqu'au délai d'expiration. La porte décide du 404.
|
|
44
|
+
* const found = await this.getResource(id);
|
|
45
|
+
* if (!found) throw new HttpError(`Book ${id} not found`, 404);
|
|
46
|
+
* return found;
|
|
43
47
|
* }
|
|
44
48
|
* }
|
|
45
49
|
* ```
|
|
@@ -117,6 +121,10 @@ var ResourceController = class extends Controller {
|
|
|
117
121
|
/**
|
|
118
122
|
* Lit une entité par id — `null` si absente (la porte décide du 404).
|
|
119
123
|
*
|
|
124
|
+
* ⚠️ Ne pas rendre ce `null` tel quel depuis une action : pour le Resolver,
|
|
125
|
+
* `null` veut dire « l'action répond elle-même », et la requête attend
|
|
126
|
+
* alors son délai d'expiration. Lever un `HttpError` 404.
|
|
127
|
+
*
|
|
120
128
|
* `options.relations` charge les associations dans la foulée. La porte doit
|
|
121
129
|
* n'y laisser passer que des relations qu'elle a DÉCLARÉES : un `include`
|
|
122
130
|
* libre laisse le client nommer n'importe quelle association, donc lire des
|
|
@@ -144,5 +152,4 @@ var ResourceController = class extends Controller {
|
|
|
144
152
|
return Promise.resolve(resource.delete(criteria));
|
|
145
153
|
}
|
|
146
154
|
};
|
|
147
|
-
//#endregion
|
|
148
155
|
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
|
-
|
|
4
|
+
// nodefony/src/Route.ts
|
|
5
5
|
/**
|
|
6
6
|
* Motif d'un segment variable : `{id}`, `/{slug}?`, `.{format}`, `{id}(\d+)`.
|
|
7
7
|
*
|
|
@@ -266,7 +266,7 @@ var Route = class Route {
|
|
|
266
266
|
const k = this.variables[i] || "wildcard";
|
|
267
267
|
param &&= decode(param);
|
|
268
268
|
const req = this.getRequirement(k);
|
|
269
|
-
let result
|
|
269
|
+
let result;
|
|
270
270
|
if (req) {
|
|
271
271
|
let compiled = this.varRegexp?.[k];
|
|
272
272
|
if (!compiled) {
|
|
@@ -549,5 +549,4 @@ var Route = class Route {
|
|
|
549
549
|
return true;
|
|
550
550
|
}
|
|
551
551
|
};
|
|
552
|
-
//#endregion
|
|
553
552
|
export { Route as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Controller from "./Controller.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/SecurityApiController.ts
|
|
3
3
|
const NO_STORE = { "cache-control": "no-store" };
|
|
4
4
|
/**
|
|
5
5
|
* Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
|
|
@@ -20,5 +20,4 @@ var SecurityApiController = class extends Controller {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
//#endregion
|
|
24
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -5,7 +5,7 @@ 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
|
-
|
|
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.
|
|
@@ -952,5 +952,4 @@ async function readCoreInfo() {
|
|
|
952
952
|
dependencies
|
|
953
953
|
};
|
|
954
954
|
}
|
|
955
|
-
//#endregion
|
|
956
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
|
-
|
|
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
|
|
@@ -126,5 +126,4 @@ async function evaluateIdempotency(opts) {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
//#endregion
|
|
130
129
|
export { IDEMPOTENCY_KEY_MAX, computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity };
|