@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,5 +1,5 @@
|
|
|
1
1
|
import { GcScheduler } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/idempotencyGc.ts
|
|
3
3
|
/**
|
|
4
4
|
* Arme un {@link GcScheduler} qui purge périodiquement les entrées expirées d'un
|
|
5
5
|
* store d'idempotence — **UNIQUEMENT si le store expose `gc()`**.
|
|
@@ -28,5 +28,4 @@ function scheduleIdempotencyGc(store, opts) {
|
|
|
28
28
|
opts.log?.(armed ? `idempotency gc armed — purge every ${opts.intervalS}s (SQL store exposes gc())` : `idempotency gc disarmed — intervalS=${opts.intervalS} (delegated to cron / external)`);
|
|
29
29
|
return scheduler;
|
|
30
30
|
}
|
|
31
|
-
//#endregion
|
|
32
31
|
export { scheduleIdempotencyGc };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/idempotencyStoreRegistry.ts
|
|
2
2
|
const factories = /* @__PURE__ */ new Map();
|
|
3
3
|
/**
|
|
4
4
|
* Enregistre (ou remplace) la fabrique d'un store d'idempotence distribué.
|
|
@@ -32,5 +32,4 @@ function listIdempotencyStores() {
|
|
|
32
32
|
function listIdempotencyBackends() {
|
|
33
33
|
return ["memory", ...factories.keys()];
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencyStores, registerIdempotencyStore };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_LAYOUTS, LOGIN_PAGE_SKINS, LOGIN_PAGE_SKIN_SPECS, LOGIN_PAGE_THEME_KEY, OAUTH2_CANCELLED_REASON, isLoginPageSkin, loginPageSkinFile, oauth2AuthorizePath, safeRedirectPath } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/loginPage.ts
|
|
3
3
|
/**
|
|
4
4
|
* Mémoïse une ressource chargée de façon asynchrone : le SUCCÈS est gardé, un
|
|
5
5
|
* ÉCHEC est oublié. Garder la promesse rejetée figerait la panne — la page
|
|
@@ -210,7 +210,7 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
|
|
|
210
210
|
</p>
|
|
211
211
|
<ul class="badges">
|
|
212
212
|
<li class="agentic">Agentic ready</li>
|
|
213
|
-
<li>Node.js ≥ 24</li>
|
|
213
|
+
<li>Node.js ≥ 24.14</li>
|
|
214
214
|
<li>TypeScript strict</li>
|
|
215
215
|
<li>ESM only</li>
|
|
216
216
|
<li>Licence Apache 2.0</li>
|
|
@@ -427,5 +427,4 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
|
|
|
427
427
|
</body>
|
|
428
428
|
</html>
|
|
429
429
|
`;
|
|
430
|
-
//#endregion
|
|
431
430
|
export { LOGIN_PAGE_ASSET_FILES, LOGIN_PAGE_EMPTY_NOTICE, LOGIN_PAGE_TEMPLATE, ORGANIZATION_ACCOUNT_LABEL, buildLoginPageView, keepSuccess };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readSessionAuthentication } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/requireAuthentication.ts
|
|
3
3
|
/**
|
|
4
4
|
* Ce qui manque à une session pour satisfaire un `@RequireAuthentication` —
|
|
5
5
|
* fonction PURE : la session, l'exigence et l'heure lui sont données.
|
|
@@ -32,5 +32,4 @@ function authenticationShortfall(session, requirement, now) {
|
|
|
32
32
|
function stepUpChallenge(requirement) {
|
|
33
33
|
return "Session realm=\"nodefony\", error=\"insufficient_user_authentication\", error_description=\"A more recent or stronger authentication is required\"" + (requirement.maxAge === null ? "" : `, max_age="${requirement.maxAge}"`);
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { authenticationShortfall, stepUpChallenge };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BootConfigurationError, SchemaValidationError, isStandardSchema, toSchemaValidationIssues } from "nodefony";
|
|
2
|
+
// nodefony/src/routeSchema.ts
|
|
3
|
+
const ROUTE_PARTS = [
|
|
4
|
+
"params",
|
|
5
|
+
"query",
|
|
6
|
+
"body",
|
|
7
|
+
"headers"
|
|
8
|
+
];
|
|
9
|
+
/** Les noms des parties, pour reconnaître une clé lue sur un objet inconnu. */
|
|
10
|
+
const ROUTE_PART_NAMES = new Set(ROUTE_PARTS);
|
|
11
|
+
/**
|
|
12
|
+
* Valide l'entrée d'une action contre son schéma — synchrone tant que chaque
|
|
13
|
+
* validateur l'est, asynchrone seulement si l'un d'eux rend une promesse.
|
|
14
|
+
*
|
|
15
|
+
* Les quatre parties sont toutes validées avant de conclure : un échec rend
|
|
16
|
+
* en une fois TOUTES les anomalies, chacune rattachée à sa partie. La valeur
|
|
17
|
+
* d'une partie validée est remplacée par la SORTIE de son schéma (valeurs par
|
|
18
|
+
* défaut, coercitions) ; la valeur reçue n'est ni copiée ni sérialisée — des
|
|
19
|
+
* octets décodés par le transport arrivent tels quels au validateur.
|
|
20
|
+
*
|
|
21
|
+
* @param schema - le schéma de la route.
|
|
22
|
+
* @param raw - l'entrée brute, mutée en place (objet propre à la requête).
|
|
23
|
+
* @returns l'entrée validée, ou sa promesse.
|
|
24
|
+
* @throws SchemaValidationError (statut 422) quand une partie est invalide —
|
|
25
|
+
* de façon synchrone, ou par la promesse rendue.
|
|
26
|
+
*/
|
|
27
|
+
function validateRouteInput(schema, raw) {
|
|
28
|
+
let failures = null;
|
|
29
|
+
let pending = null;
|
|
30
|
+
for (let i = 0; i < ROUTE_PARTS.length; i++) {
|
|
31
|
+
const part = ROUTE_PARTS[i];
|
|
32
|
+
const partSchema = part === void 0 ? void 0 : schema[part];
|
|
33
|
+
if (part === void 0 || partSchema === void 0) continue;
|
|
34
|
+
const result = partSchema["~standard"].validate(raw[part]);
|
|
35
|
+
if (result instanceof Promise) {
|
|
36
|
+
pending ??= [];
|
|
37
|
+
pending.push(result.then((settled) => failures = applyPartResult(raw, part, i, settled, failures)));
|
|
38
|
+
} else failures = applyPartResult(raw, part, i, result, failures);
|
|
39
|
+
}
|
|
40
|
+
if (pending === null) return concludeRouteInput(raw, failures);
|
|
41
|
+
return Promise.all(pending).then(() => concludeRouteInput(raw, failures));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Applique le résultat d'une partie : sa sortie remplace la valeur brute, ou
|
|
45
|
+
* ses anomalies sont rangées à SON rang — l'erreur les rend dans l'ordre des
|
|
46
|
+
* parties, que leurs validateurs aient attendu ou non.
|
|
47
|
+
*/
|
|
48
|
+
function applyPartResult(raw, part, rank, result, failures) {
|
|
49
|
+
if (result.issues === void 0) {
|
|
50
|
+
raw[part] = result.value;
|
|
51
|
+
return failures;
|
|
52
|
+
}
|
|
53
|
+
const ranked = failures ?? [];
|
|
54
|
+
ranked[rank] = toSchemaValidationIssues(result.issues, part);
|
|
55
|
+
return ranked;
|
|
56
|
+
}
|
|
57
|
+
/** Rend l'entrée validée, ou lève l'erreur unique qui porte toutes les anomalies. */
|
|
58
|
+
function concludeRouteInput(raw, failures) {
|
|
59
|
+
if (failures === null) return raw;
|
|
60
|
+
const issues = [];
|
|
61
|
+
for (const partIssues of failures) if (partIssues !== void 0) issues.push(...partIssues);
|
|
62
|
+
throw new SchemaValidationError(issues);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Refuse, au démarrage, un schéma de route mal formé : partie inconnue (une
|
|
66
|
+
* faute de frappe — `bodies` — serait sinon ignorée sans un mot), ou valeur
|
|
67
|
+
* qui n'implémente pas Standard Schema (un appelant JavaScript peut passer
|
|
68
|
+
* n'importe quoi).
|
|
69
|
+
*
|
|
70
|
+
* @param schema - le schéma déclaré.
|
|
71
|
+
* @param routeName - la route, nommée dans l'erreur.
|
|
72
|
+
* @throws BootConfigurationError sur la première faute trouvée.
|
|
73
|
+
*/
|
|
74
|
+
function assertRouteSchemaShape(schema, routeName) {
|
|
75
|
+
if (typeof schema !== "object" || schema === null) throw new BootConfigurationError(`Route "${routeName}" : l'option « schema » doit être un objet { params, query, body, headers }.`);
|
|
76
|
+
for (const [part, value] of Object.entries(schema)) {
|
|
77
|
+
if (!ROUTE_PART_NAMES.has(part)) throw new BootConfigurationError(`Route "${routeName}" : partie de schéma inconnue « ${part} » (attendu : ${ROUTE_PARTS.join(", ")}).`);
|
|
78
|
+
if (value !== void 0 && !isStandardSchema(value)) throw new BootConfigurationError(`Route "${routeName}" : « schema.${part} » n'implémente pas Standard Schema (Zod, Valibot, ArkType…).`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export { assertRouteSchemaShape, validateRouteInput };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractActionScopes } from "../decorators/routerDecorators.js";
|
|
2
2
|
import router_default from "../service/router.js";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/scopeCatalog.ts
|
|
4
4
|
/**
|
|
5
5
|
* P6.8 — **Découverte au boot** : scanne TOUTES les routes montées (`Router.routes`)
|
|
6
6
|
* et agrège les scopes déclarés par `@RequireScope`, **regroupés par API** (préfixe
|
|
@@ -36,5 +36,4 @@ function collectDeclaredApiScopes() {
|
|
|
36
36
|
scopes: [...byApi.get(api) ?? []].sort()
|
|
37
37
|
}));
|
|
38
38
|
}
|
|
39
|
-
//#endregion
|
|
40
39
|
export { collectDeclaredApiScopes, collectDeclaredApiScopes as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/singletonGuard.ts
|
|
2
2
|
/**
|
|
3
3
|
* Filets d'un contrôleur **singleton** — le défaut : UNE instance par classe,
|
|
4
4
|
* partagée par toutes les requêtes, concurrentes comprises.
|
|
@@ -72,5 +72,4 @@ function guardSingletonState(controller, name) {
|
|
|
72
72
|
}
|
|
73
73
|
Object.preventExtensions(controller);
|
|
74
74
|
}
|
|
75
|
-
//#endregion
|
|
76
75
|
export { guardSingletonState, isDevelopment, singletonWriteMessage };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FLOW_STEPS, SEVERITY_NAMES } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/syslogFilters.ts
|
|
3
3
|
/**
|
|
4
4
|
* Ce que le journal sait TRIER — un seul axe, le temps.
|
|
5
5
|
*
|
|
@@ -29,6 +29,8 @@ const SYSLOG_FILTERS = {
|
|
|
29
29
|
module: "string",
|
|
30
30
|
/** Catégorie de message (msgid) — inclusion insensible à la casse. */
|
|
31
31
|
msgid: "string",
|
|
32
|
+
/** Module exclu — égalité insensible à la casse (`NODEFONY` = le framework). */
|
|
33
|
+
excludeModule: "string",
|
|
32
34
|
/** Protocole d'origine ; absent = les deux. */
|
|
33
35
|
protocol: ["ws", "http"],
|
|
34
36
|
/**
|
|
@@ -42,10 +44,13 @@ const SYSLOG_FILTERS = {
|
|
|
42
44
|
* seconde liste ne peut se périmer.
|
|
43
45
|
*/
|
|
44
46
|
flow: { each: Object.keys(FLOW_STEPS) },
|
|
47
|
+
/** Écarte les lignes d'accès de la console d'administration elle-même. */
|
|
48
|
+
hideConsole: "boolean",
|
|
49
|
+
/** Une ligne par requête (sa ligne d'accès), avec le nombre de lignes regroupées. */
|
|
50
|
+
groupByRequest: "boolean",
|
|
45
51
|
/** Borne basse d'horodatage (epoch ms, incluse). */
|
|
46
52
|
from: "int",
|
|
47
53
|
/** Borne haute d'horodatage (epoch ms, incluse). */
|
|
48
54
|
to: "int"
|
|
49
55
|
};
|
|
50
|
-
//#endregion
|
|
51
56
|
export { SYSLOG_FILTERS, SYSLOG_SORTABLE };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Kernel, Module } from "nodefony";
|
|
2
2
|
import type { IFrameworkConfigInput, IFrameworkConfig } from "./nodefony/config/config.js";
|
|
3
3
|
import { getIdempotencyStoreFactory, registerIdempotencyStore, listIdempotencyStores } from "./nodefony/src/idempotencyStoreRegistry.js";
|
|
4
|
-
import Router from "./nodefony/service/router.js";
|
|
4
|
+
import Router, { FRAMEWORK_ROUTER, isFrameworkRouter } from "./nodefony/service/router.js";
|
|
5
5
|
import Route from "./nodefony/src/Route.js";
|
|
6
6
|
import Controller from "./nodefony/src/Controller.js";
|
|
7
7
|
import ResourceController from "./nodefony/src/ResourceController.js";
|
|
@@ -24,9 +24,10 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
|
|
|
24
24
|
import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
|
|
25
25
|
import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
|
|
26
26
|
import Eta from "./nodefony/service/Eta.js";
|
|
27
|
-
import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
|
|
28
|
-
import { mergeSchemas, makeExecutableSchema } from "@graphql-tools/schema";
|
|
29
27
|
import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta } from "./nodefony/decorators/routerDecorators.js";
|
|
28
|
+
export type { IHttpMethodDecorator } from "./nodefony/decorators/routerDecorators.js";
|
|
29
|
+
export { validateRouteInput } from "./nodefony/src/routeSchema.js";
|
|
30
|
+
export type { IRouteSchema, RouteInput, RoutePart, RawRouteInput, RawRouteParams, RawRouteQuery, RawRouteHeaders, } from "./nodefony/src/routeSchema.js";
|
|
30
31
|
declare module "nodefony" {
|
|
31
32
|
interface NodefonyModuleConfig {
|
|
32
33
|
"@nodefony/framework": IFrameworkConfigInput;
|
|
@@ -77,14 +78,8 @@ declare class Framework extends Module<IFrameworkConfig> {
|
|
|
77
78
|
*/
|
|
78
79
|
onKernelReady(): Promise<this>;
|
|
79
80
|
}
|
|
80
|
-
declare const graphql: {
|
|
81
|
-
mergeSchemas: typeof mergeSchemas;
|
|
82
|
-
makeExecutableSchema: typeof makeExecutableSchema;
|
|
83
|
-
mergeResolvers: typeof mergeResolvers;
|
|
84
|
-
mergeTypeDefs: typeof mergeTypeDefs;
|
|
85
|
-
};
|
|
86
81
|
export default Framework;
|
|
87
|
-
export { Controller, ResourceController, Route, Router, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta,
|
|
82
|
+
export { Controller, ResourceController, Route, Router, FRAMEWORK_ROUTER, isFrameworkRouter, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
|
|
88
83
|
export { frameworkConfigSchema } from "./nodefony/config/config.js";
|
|
89
84
|
export type { IdempotencyStoreFactory, IIdempotencyStoreFactoryContext, } from "./nodefony/src/idempotencyStoreRegistry.js";
|
|
90
85
|
export type { SecurityClause, SecurityRequirement, IAuthenticationRequirement, IRequireAuthenticationOptions, CspDirectives, IParamArgSource, ParamMeta, } from "./nodefony/decorators/routerDecorators.js";
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { ContextType } from "@nodefony/http";
|
|
2
2
|
import Controller from "../src/Controller.js";
|
|
3
|
+
type AdminQuery = Record<string, string | string[]>;
|
|
4
|
+
/**
|
|
5
|
+
* Ramène les paramètres d'URL au contrat {@link IAdminRequest.query} : seules
|
|
6
|
+
* les chaînes et les tableaux de chaînes passent. `qs` produit des objets pour
|
|
7
|
+
* `a[b]=1` ; les laisser passer sous un type qui les nie tromperait chaque
|
|
8
|
+
* producteur du plan d'administration.
|
|
9
|
+
*
|
|
10
|
+
* @param source - paramètres d'URL de la requête (`Controller.query`)
|
|
11
|
+
* @returns `source` lui-même quand il respecte déjà le contrat (aucune
|
|
12
|
+
* allocation), sinon une copie sans les valeurs imbriquées
|
|
13
|
+
*/
|
|
14
|
+
export declare function toAdminQuery(source: Record<string, unknown> | undefined): AdminQuery;
|
|
3
15
|
/**
|
|
4
16
|
* Controller pont unique du data plane admin (Studio).
|
|
5
17
|
*
|
|
@@ -23,7 +35,7 @@ declare class AdminApiController extends Controller {
|
|
|
23
35
|
constructor(context: ContextType);
|
|
24
36
|
/**
|
|
25
37
|
* Action générique appelée par le Resolver pour toute route admin — DUPLEX
|
|
26
|
-
* (
|
|
38
|
+
* (une action, deux transports) : même exécution sur les deux transports, seul
|
|
27
39
|
* l'emballage diffère.
|
|
28
40
|
*
|
|
29
41
|
* - **HTTP** : rendu historique inchangé (`renderJson` + status + header
|
|
@@ -91,6 +91,13 @@ declare class SessionAuthController extends SecurityApiController {
|
|
|
91
91
|
* Identité de la session courante, re-résolue (rôles frais). Toujours `200` :
|
|
92
92
|
* `{ user: null }` quand il n'y a pas de session (ou qu'elle ne mène plus à
|
|
93
93
|
* un compte actif) — l'absence de session est un état, pas une erreur.
|
|
94
|
+
*
|
|
95
|
+
* **Sème aussi le jeton anti-CSRF** (cookie lisible `csrf-token`) : c'est la
|
|
96
|
+
* seule requête sûre que TOUT client connaît d'avance. Sans elle, une mutation
|
|
97
|
+
* `@CsrfProtect` n'a de jeton à rejouer que si son auteur a AUSSI décoré une
|
|
98
|
+
* lecture — protéger les seules écritures, le geste naturel, rendait alors 403
|
|
99
|
+
* sans issue, au navigateur comme aux tests générés. Le jeton est sans état
|
|
100
|
+
* (HMAC) : celui-ci vaut pour toute route protégée.
|
|
94
101
|
*/
|
|
95
102
|
me(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
|
|
96
103
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
+
import type { Readable } from "node:stream";
|
|
2
3
|
import { TypeController } from "../service/router.js";
|
|
3
4
|
import { RouteOptions } from "../src/Route.js";
|
|
4
5
|
import Controller from "../src/Controller.js";
|
|
5
6
|
import type { ControllerScope } from "../src/Controller.js";
|
|
6
7
|
import { Module } from "nodefony";
|
|
7
8
|
import { ControllerConstructor } from "../src/Route.js";
|
|
9
|
+
import type { IRouteSchema, RawRouteInput, RouteInput } from "../src/routeSchema.js";
|
|
8
10
|
import type { SessionIntent } from "@nodefony/http";
|
|
9
11
|
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
10
12
|
/**
|
|
@@ -66,7 +68,30 @@ declare function controller(prefix: string): <T extends ControllerConstructor &
|
|
|
66
68
|
* return this.renderJson({ name });
|
|
67
69
|
* }
|
|
68
70
|
*/
|
|
69
|
-
declare function route(name: string, options: RouteOptions
|
|
71
|
+
declare function route(name: string, options: RouteOptions & {
|
|
72
|
+
schema?: undefined;
|
|
73
|
+
bodyStream?: false;
|
|
74
|
+
}): RouteMethodDecorator;
|
|
75
|
+
declare function route<S extends IRouteSchema>(name: string, options: RouteOptionsWithSchema<S> & {
|
|
76
|
+
bodyStream?: false;
|
|
77
|
+
}): SchemaRouteMethodDecorator<S, false>;
|
|
78
|
+
declare function route<S extends IRouteSchema & {
|
|
79
|
+
readonly body?: undefined;
|
|
80
|
+
}>(name: string, options: RouteOptionsWithSchema<S> & {
|
|
81
|
+
bodyStream: true;
|
|
82
|
+
}): SchemaRouteMethodDecorator<S, true>;
|
|
83
|
+
/** Options d'une route qui déclare un schéma. */
|
|
84
|
+
type RouteOptionsWithSchema<S extends IRouteSchema> = RouteOptions & {
|
|
85
|
+
schema: S;
|
|
86
|
+
};
|
|
87
|
+
/** Décorateur de méthode d'une route sans schéma : la signature de l'action est libre. */
|
|
88
|
+
type RouteMethodDecorator = (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
89
|
+
/**
|
|
90
|
+
* Décorateur d'une action à schéma : la compilation refuse une action dont
|
|
91
|
+
* l'argument ne reçoit pas `RouteInput<S, Stream>` (champ absent du schéma,
|
|
92
|
+
* type divergent, second argument).
|
|
93
|
+
*/
|
|
94
|
+
type SchemaRouteMethodDecorator<S extends IRouteSchema, Stream extends boolean> = <T extends (input: RouteInput<S, Stream>) => unknown>(target: object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
|
|
70
95
|
export declare const HTTP_CODE_METADATA = "route:httpCode";
|
|
71
96
|
export declare const HEADERS_METADATA = "route:responseHeaders";
|
|
72
97
|
export declare const REDIRECT_METADATA = "route:redirect";
|
|
@@ -158,19 +183,40 @@ export interface IdempotentMeta {
|
|
|
158
183
|
readonly required: boolean;
|
|
159
184
|
}
|
|
160
185
|
type MethodDecoratorOptions = Omit<RouteOptions, "path" | "method">;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Un décorateur de méthode HTTP (`@Get`, `@Post`…). Sans schéma, la signature
|
|
188
|
+
* de l'action est libre (décorateurs de paramètre) ; avec `schema`, l'action
|
|
189
|
+
* reçoit un argument unique `RouteInput<S>`, vérifié à la compilation.
|
|
190
|
+
*/
|
|
191
|
+
export interface IHttpMethodDecorator {
|
|
192
|
+
(path?: string, options?: MethodDecoratorOptions & {
|
|
193
|
+
schema?: undefined;
|
|
194
|
+
bodyStream?: false;
|
|
195
|
+
}): RouteMethodDecorator;
|
|
196
|
+
<S extends IRouteSchema>(path: string, options: MethodDecoratorOptions & {
|
|
197
|
+
schema: S;
|
|
198
|
+
bodyStream?: false;
|
|
199
|
+
}): SchemaRouteMethodDecorator<S, false>;
|
|
200
|
+
<S extends IRouteSchema & {
|
|
201
|
+
readonly body?: undefined;
|
|
202
|
+
}>(path: string, options: MethodDecoratorOptions & {
|
|
203
|
+
schema: S;
|
|
204
|
+
bodyStream: true;
|
|
205
|
+
}): SchemaRouteMethodDecorator<S, true>;
|
|
206
|
+
}
|
|
207
|
+
declare const Get: IHttpMethodDecorator;
|
|
208
|
+
declare const Post: IHttpMethodDecorator;
|
|
209
|
+
declare const Put: IHttpMethodDecorator;
|
|
210
|
+
declare const Delete: IHttpMethodDecorator;
|
|
211
|
+
declare const Patch: IHttpMethodDecorator;
|
|
212
|
+
declare const Options: IHttpMethodDecorator;
|
|
213
|
+
declare const Head: IHttpMethodDecorator;
|
|
168
214
|
/**
|
|
169
215
|
* `@All` — route sans restriction de méthode : matche **toutes** les méthodes
|
|
170
216
|
* HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
|
|
171
217
|
* `Route.matchRequirements` ne lève jamais 405 sur la méthode.
|
|
172
218
|
*/
|
|
173
|
-
declare
|
|
219
|
+
declare const All: IHttpMethodDecorator;
|
|
174
220
|
/**
|
|
175
221
|
* Fixe le code de statut HTTP de la réponse d'une action.
|
|
176
222
|
*
|
|
@@ -470,26 +516,18 @@ declare const Query: (key?: string) => (target: object, propertyKey: string, par
|
|
|
470
516
|
*
|
|
471
517
|
* ## Le corps n'est PAS validé — le type écrit ici ne promet rien
|
|
472
518
|
*
|
|
473
|
-
* `@Body()` injecte le corps **tel qu'il a été
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* taxerait toute requête à paramètres décorés).
|
|
478
|
-
*
|
|
479
|
-
* Où valider, donc :
|
|
519
|
+
* `@Body()` injecte le corps **tel qu'il a été décodé** (objet JSON ou de
|
|
520
|
+
* formulaire, chaîne pour `text/*`, octets `Uint8Array` pour tout autre type) :
|
|
521
|
+
* `@Body() dto: CreateOrder` compile, mais rien ne garantit qu'un `CreateOrder`
|
|
522
|
+
* soit arrivé.
|
|
480
523
|
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
|
|
486
|
-
* modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
|
|
487
|
-
* devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
|
|
488
|
-
* quel message, quelle règle.
|
|
524
|
+
* Pour un contrat vérifié, déclarer l'option de route `schema` (ADR-0018) : le
|
|
525
|
+
* corps est validé AVANT l'action — 422 qui nomme chaque champ —, sur HTTP comme
|
|
526
|
+
* sur le pont WebSocket, et l'action le reçoit typé dans `input.body`. Une action
|
|
527
|
+
* à schéma ne prend aucun décorateur de paramètre.
|
|
489
528
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
|
|
529
|
+
* Sans schéma, `schema.parse(body)` en première ligne de l'action reste le geste
|
|
530
|
+
* d'un cas isolé : une `ZodError` qui remonte devient le même **422**.
|
|
493
531
|
*
|
|
494
532
|
* @example
|
|
495
533
|
* ```ts
|
|
@@ -524,20 +562,21 @@ export interface IParamArgContext {
|
|
|
524
562
|
/**
|
|
525
563
|
* Query de l'invocation courante quand elle ne vient PAS de l'URL du
|
|
526
564
|
* transport — pont WS-RPC `api.request` (`Resolver.queryOverride`). Prime
|
|
527
|
-
* sur `request.
|
|
565
|
+
* sur `request.query` pour `@Query` uniquement (`@Req` reste le brut).
|
|
528
566
|
*/
|
|
529
567
|
queryOverride?: Record<string, unknown> | undefined;
|
|
530
568
|
request?: {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
569
|
+
query?: Record<string, unknown> | undefined;
|
|
570
|
+
/** Corps décodé : objet JSON, champs de formulaire, octets… */
|
|
571
|
+
body?: unknown;
|
|
572
|
+
files?: readonly unknown[] | undefined;
|
|
534
573
|
headers?: Record<string, unknown> | undefined;
|
|
535
574
|
/**
|
|
536
575
|
* P2.9 — `IncomingMessage` brut (un `Readable`) sous-jacent au wrapper
|
|
537
576
|
* `HttpRequest`. Injecté tel quel par `@Body({ stream: true })` (le pipeline
|
|
538
577
|
* n'a pas consommé/parsé ce flux). `undefined` pour les contextes WS.
|
|
539
578
|
*/
|
|
540
|
-
request?:
|
|
579
|
+
request?: Readable | undefined;
|
|
541
580
|
} | null | undefined;
|
|
542
581
|
response?: unknown;
|
|
543
582
|
session?: {
|
|
@@ -545,6 +584,31 @@ export interface IParamArgContext {
|
|
|
545
584
|
} | null | undefined;
|
|
546
585
|
getRequestCookies(name?: string): unknown;
|
|
547
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Le corps DÉCODÉ d'une invocation, depuis la requête seule — la règle de
|
|
589
|
+
* {@link readBody}, pour qui n'a pas de contexte de paramètres (porte
|
|
590
|
+
* d'idempotence).
|
|
591
|
+
*
|
|
592
|
+
* @param request - la requête du contexte.
|
|
593
|
+
* @returns le corps posé par le pont WebSocket, sinon le corps HTTP lu.
|
|
594
|
+
*/
|
|
595
|
+
export declare function readInvocationBody(request: IParamArgContext["request"]): unknown;
|
|
596
|
+
/**
|
|
597
|
+
* L'entrée BRUTE d'une action à schéma, lue aux MÊMES sources que les
|
|
598
|
+
* décorateurs de paramètre ({@link readQuery}, {@link readBody}) : pont
|
|
599
|
+
* WebSocket compris. Seule allocation de la voie à schéma : cet objet, propre
|
|
600
|
+
* à l'invocation, que la validation complète en place et que l'action reçoit.
|
|
601
|
+
*
|
|
602
|
+
* Arguments POSITIONNELS, pas un objet de contexte : la voie à schéma n'en
|
|
603
|
+
* alloue aucun de plus.
|
|
604
|
+
*
|
|
605
|
+
* @param paramsMap - variables de route nommées.
|
|
606
|
+
* @param request - la requête du contexte.
|
|
607
|
+
* @param queryOverride - query du chemin invoqué (pont WebSocket), sinon rien.
|
|
608
|
+
* @param bodyStream - `true` : `body` est le flux brut, jamais lu.
|
|
609
|
+
* @returns les quatre parties, brutes.
|
|
610
|
+
*/
|
|
611
|
+
export declare function readRouteInput(paramsMap: Record<string, unknown>, request: IParamArgContext["request"], queryOverride: Record<string, unknown> | undefined, bodyStream: boolean): RawRouteInput;
|
|
548
612
|
/**
|
|
549
613
|
* Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
|
|
550
614
|
*
|
|
@@ -605,6 +669,36 @@ export declare function buildContextParamArgs(metas: readonly ParamMeta[], conte
|
|
|
605
669
|
* @returns les métadonnées, ou `null` — une action sans décorateur ne paie rien
|
|
606
670
|
*/
|
|
607
671
|
export declare function getParamArgsMeta(proto: object, method: string | symbol): readonly ParamMeta[] | null;
|
|
672
|
+
/**
|
|
673
|
+
* Le schéma d'entrée d'une méthode (option de route `schema`), ou `null`.
|
|
674
|
+
* Lecture `Reflect` : à faire une fois, hors du chemin de chaque requête.
|
|
675
|
+
*
|
|
676
|
+
* @param proto - le prototype de la classe.
|
|
677
|
+
* @param method - le nom de la méthode.
|
|
678
|
+
* @returns le schéma, ou `null` — une action sans schéma ne paie rien.
|
|
679
|
+
* @throws BootConfigurationError si le schéma rangé est mal formé.
|
|
680
|
+
*/
|
|
681
|
+
export declare function getRouteSchema(proto: object, method: string): IRouteSchema | null;
|
|
682
|
+
/**
|
|
683
|
+
* Refuse, au DÉMARRAGE, une action à schéma mal déclarée — appelé par
|
|
684
|
+
* `Router.setController` pour chaque route du contrôleur, pour qu'une faute
|
|
685
|
+
* ne se révèle pas à la première requête :
|
|
686
|
+
*
|
|
687
|
+
* - schéma mal formé (partie inconnue, valeur qui n'est pas un Standard Schema) ;
|
|
688
|
+
* - schéma ET décorateurs de paramètre (`@Body`…) : une action à schéma reçoit
|
|
689
|
+
* un argument UNIQUE, les deux formes ne se combinent pas ;
|
|
690
|
+
* - `bodyStream: true` ET `schema.body` : un flux ne se valide pas.
|
|
691
|
+
*
|
|
692
|
+
* @param routeDef - la route (nom, contrôleur, méthode).
|
|
693
|
+
* @throws BootConfigurationError qui nomme la route.
|
|
694
|
+
*/
|
|
695
|
+
export declare function assertRouteSchemaDeclaration(routeDef: {
|
|
696
|
+
name: string;
|
|
697
|
+
controller?: {
|
|
698
|
+
prototype: object;
|
|
699
|
+
} | null | undefined;
|
|
700
|
+
classMethod?: string | undefined;
|
|
701
|
+
}): void;
|
|
608
702
|
/**
|
|
609
703
|
* P2.9 — Indique si l'action d'une route attend le **flux brut** du body
|
|
610
704
|
* (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
|
|
@@ -631,6 +725,13 @@ declare function routeExpectsBodyStream(routeDef: {
|
|
|
631
725
|
export interface RouteActionMeta {
|
|
632
726
|
/** Paramètres décorés (`@Param`/`@Body`/`@Query`…) — `null` si aucun. */
|
|
633
727
|
paramsMeta: readonly ParamMeta[] | null;
|
|
728
|
+
/**
|
|
729
|
+
* Schéma d'entrée (`schema` de l'option de route) — `null` si l'action n'en
|
|
730
|
+
* déclare pas (cas courant → une comparaison, aucune allocation).
|
|
731
|
+
*/
|
|
732
|
+
schema: IRouteSchema | null;
|
|
733
|
+
/** `bodyStream: true` déclaré en option de route. */
|
|
734
|
+
bodyStream: boolean;
|
|
634
735
|
/** `@Redirect` de l'action — `null` si absent. */
|
|
635
736
|
redirectMeta: RedirectMeta | null;
|
|
636
737
|
/** `@HttpCode` de l'action — `null` si absent. */
|
|
@@ -10,10 +10,9 @@ export interface IController {
|
|
|
10
10
|
context?: ContextType | undefined;
|
|
11
11
|
session?: Session | null;
|
|
12
12
|
method?: HTTPMethod | undefined;
|
|
13
|
-
queryGet: Record<string, unknown>;
|
|
14
13
|
query: Record<string, unknown>;
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
files: readonly unknown[];
|
|
15
|
+
body: Record<string, unknown>;
|
|
17
16
|
module?: Module | undefined;
|
|
18
17
|
setContext(context: ContextType): void;
|
|
19
18
|
setContextJson(encoding?: BufferEncoding): unknown;
|
|
@@ -6,7 +6,16 @@ import type { IRequestRouter } from "@nodefony/http";
|
|
|
6
6
|
import Resolver from "../src/Resolver.js";
|
|
7
7
|
import Controller from "../src/Controller.js";
|
|
8
8
|
export type TypeController<T> = new (...args: any[]) => T;
|
|
9
|
+
/**
|
|
10
|
+
* Marque du Router de `@nodefony/framework` — ce que {@link isFrameworkRouter}
|
|
11
|
+
* lit. `Symbol.for` : la même marque pour deux copies installées du framework,
|
|
12
|
+
* là où `instanceof` échouerait en silence. Un faux routeur de test s'en
|
|
13
|
+
* réclame explicitement.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FRAMEWORK_ROUTER: unique symbol;
|
|
9
16
|
declare class Router extends Service implements IRequestRouter {
|
|
17
|
+
/** Marque {@link FRAMEWORK_ROUTER} — sur le prototype, aucun champ par instance. */
|
|
18
|
+
get [FRAMEWORK_ROUTER](): true;
|
|
10
19
|
static routes: Route[];
|
|
11
20
|
routes: Route[];
|
|
12
21
|
private singletonControllers;
|
|
@@ -55,7 +64,10 @@ declare class Router extends Service implements IRequestRouter {
|
|
|
55
64
|
* WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
|
|
56
65
|
* GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
|
|
57
66
|
* `undefined` (GET/HTTP) → match historique sur `context.method`.
|
|
58
|
-
* @returns un `Resolver` (`.resolve === true` si une route a matché)
|
|
67
|
+
* @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
|
|
68
|
+
* pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
|
|
69
|
+
* `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
|
|
70
|
+
* le pipeline le lève après le pare-feu.
|
|
59
71
|
*/
|
|
60
72
|
resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): Resolver;
|
|
61
73
|
resolveController(contex: ContextType, name: string): Resolver;
|
|
@@ -89,4 +101,18 @@ declare class Router extends Service implements IRequestRouter {
|
|
|
89
101
|
*/
|
|
90
102
|
static getRoutesForController(myconstructor: TypeController<Controller>): Route[];
|
|
91
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Dit si le service `router` d'un contexte est le Router de
|
|
106
|
+
* `@nodefony/framework` — la garde qui rétrécit le contrat `IRequestRouter` de
|
|
107
|
+
* `@nodefony/http` sans conversion, pour qui lit des membres propres au
|
|
108
|
+
* framework (le Resolver complet que rend `resolve`).
|
|
109
|
+
*
|
|
110
|
+
* Lit la marque {@link FRAMEWORK_ROUTER}, jamais `instanceof` : deux copies
|
|
111
|
+
* installées du framework feraient échouer `instanceof` en silence. Une
|
|
112
|
+
* lecture de propriété.
|
|
113
|
+
*
|
|
114
|
+
* @param router - le routeur lu sur le contexte (`context.router`).
|
|
115
|
+
* @returns `true` si c'est le Router du framework.
|
|
116
|
+
*/
|
|
117
|
+
export declare function isFrameworkRouter(router: IRequestRouter | null | undefined): router is Router;
|
|
92
118
|
export default Router;
|