@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4
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 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +15 -20
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +45 -11
- package/dist/nodefony/controller/ApiKeyController.js +18 -10
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +117 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -17
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +50 -13
- package/dist/nodefony/controller/TokenAuthController.js +14 -8
- package/dist/nodefony/controller/TotpController.js +7 -8
- package/dist/nodefony/controller/WebAuthnController.js +28 -12
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +268 -65
- 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 +97 -34
- package/dist/nodefony/src/Controller.js +71 -43
- package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
- package/dist/nodefony/src/KernelAdminApi.js +82 -18
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +105 -36
- package/dist/nodefony/src/ResourceController.js +2 -3
- package/dist/nodefony/src/Route.js +54 -8
- package/dist/nodefony/src/SecurityApiController.js +23 -0
- 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 +39 -23
- package/dist/nodefony/src/idempotency.js +14 -22
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +430 -0
- package/dist/nodefony/src/requireAuthentication.js +35 -0
- 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 +1 -2
- package/dist/types/index.d.ts +8 -12
- package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
- package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
- package/dist/types/nodefony/service/router.d.ts +40 -1
- package/dist/types/nodefony/src/Controller.d.ts +54 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +44 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
- package/dist/types/nodefony/src/Route.d.ts +21 -1
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
- package/dist/types/nodefony/src/docsReader.d.ts +10 -6
- package/dist/types/nodefony/src/idempotency.d.ts +12 -10
- package/dist/types/nodefony/src/loginPage.d.ts +144 -0
- package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/docs/admin.md +83 -78
- package/docs/controller.md +112 -111
- package/docs/decorateurs.md +212 -163
- package/docs/idempotence.md +137 -135
- package/docs/routing.md +96 -95
- package/docs/templates.md +55 -55
- package/package.json +10 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { assertRouteSchemaShape } from "../src/routeSchema.js";
|
|
1
2
|
import Controller from "../src/Controller.js";
|
|
2
3
|
import router_default from "../service/router.js";
|
|
3
|
-
import { RequestContext } from "nodefony";
|
|
4
|
+
import { BootConfigurationError, RequestContext } from "nodefony";
|
|
4
5
|
import "reflect-metadata";
|
|
5
|
-
|
|
6
|
+
// nodefony/decorators/routerDecorators.ts
|
|
6
7
|
const metadataKey = "routes:definitions";
|
|
7
8
|
/**
|
|
8
9
|
* Noms déjà portés par `Controller` — méthodes ET accesseurs, les siens comme
|
|
@@ -134,7 +135,7 @@ function controller(prefix) {
|
|
|
134
135
|
const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
|
|
135
136
|
if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
|
|
136
137
|
}
|
|
137
|
-
if (options.path
|
|
138
|
+
if (options.path === "*") {
|
|
138
139
|
hasMagic = {
|
|
139
140
|
options,
|
|
140
141
|
name
|
|
@@ -149,26 +150,19 @@ function controller(prefix) {
|
|
|
149
150
|
return mycontroller;
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Crée une route avec le nom et les options spécifiés.
|
|
154
|
-
*
|
|
155
|
-
* @param name - Le nom de la route.
|
|
156
|
-
* @param options - Les options de la route.
|
|
157
|
-
* @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* \@route("myroute", {
|
|
161
|
-
* path: "/add/{name}",
|
|
162
|
-
* method: ["GET", "POST"],
|
|
163
|
-
* defaults: { name: "john" },
|
|
164
|
-
* })
|
|
165
|
-
* method(name: string) {
|
|
166
|
-
* return this.renderJson({ name });
|
|
167
|
-
* }
|
|
168
|
-
*/
|
|
169
153
|
function route(name, options) {
|
|
154
|
+
return defineRoute(name, options);
|
|
155
|
+
}
|
|
156
|
+
/** L'implémentation de {@link route}, sans les surcharges de typage. */
|
|
157
|
+
function defineRoute(name, options) {
|
|
170
158
|
return function(target, propertyKey, descriptor) {
|
|
171
159
|
assertActionNameFree(target, propertyKey);
|
|
160
|
+
if (options.schema !== void 0) {
|
|
161
|
+
const previous = Reflect.getOwnMetadata(ROUTE_SCHEMA_METADATA, target, propertyKey);
|
|
162
|
+
if (previous !== void 0 && previous !== options.schema) throw new BootConfigurationError(`${target.constructor.name}.${propertyKey} : deux routes de la même action déclarent des schémas différents — une action n'a qu'une entrée, déclarer le schéma une seule fois.`);
|
|
163
|
+
Reflect.defineMetadata(ROUTE_SCHEMA_METADATA, options.schema, target, propertyKey);
|
|
164
|
+
}
|
|
165
|
+
if (options.bodyStream === true) Reflect.defineMetadata(BODY_STREAM_METADATA, true, target, propertyKey);
|
|
172
166
|
const className = target.constructor.name;
|
|
173
167
|
const classMethod = propertyKey;
|
|
174
168
|
const prefix = options.prefix || null;
|
|
@@ -183,7 +177,7 @@ function route(name, options) {
|
|
|
183
177
|
} catch (error) {
|
|
184
178
|
filePath = error;
|
|
185
179
|
}
|
|
186
|
-
const metadata = Reflect.getMetadata(metadataKey, target.constructor)
|
|
180
|
+
const metadata = { ...Reflect.getMetadata(metadataKey, target.constructor) };
|
|
187
181
|
metadata[name] = {
|
|
188
182
|
path,
|
|
189
183
|
filePath,
|
|
@@ -195,7 +189,8 @@ function route(name, options) {
|
|
|
195
189
|
host: options.host,
|
|
196
190
|
defaults: options.defaults,
|
|
197
191
|
requirements: options.requirements,
|
|
198
|
-
bypassFirewall: options.bypassFirewall
|
|
192
|
+
bypassFirewall: options.bypassFirewall,
|
|
193
|
+
fallback: options.fallback
|
|
199
194
|
};
|
|
200
195
|
Reflect.defineMetadata(metadataKey, metadata, target.constructor);
|
|
201
196
|
return descriptor;
|
|
@@ -210,6 +205,10 @@ function extractControllerFilePath(stackTrace) {
|
|
|
210
205
|
const HTTP_CODE_METADATA = "route:httpCode";
|
|
211
206
|
const HEADERS_METADATA = "route:responseHeaders";
|
|
212
207
|
const REDIRECT_METADATA = "route:redirect";
|
|
208
|
+
/** Schéma d'entrée d'une action (`schema` d'une option de route) — par méthode. */
|
|
209
|
+
const ROUTE_SCHEMA_METADATA = "route:schema";
|
|
210
|
+
/** `bodyStream: true` d'une option de route — par méthode. */
|
|
211
|
+
const BODY_STREAM_METADATA = "route:bodyStream";
|
|
213
212
|
const PARAM_ARGS_METADATA = "route:paramArgs";
|
|
214
213
|
const DOMAIN_CLASS_METADATA = "route:domainClass";
|
|
215
214
|
const DOMAIN_METHOD_METADATA = "route:domainMethod";
|
|
@@ -231,7 +230,7 @@ function httpMethodDecorator(methods) {
|
|
|
231
230
|
const declaredMethods = options.requirements?.methods;
|
|
232
231
|
const added = declaredMethods === void 0 ? [] : Array.isArray(declaredMethods) ? declaredMethods : [declaredMethods];
|
|
233
232
|
const fusion = added.length > 0 ? [.../* @__PURE__ */ new Set([...methods, ...added])] : methods;
|
|
234
|
-
return
|
|
233
|
+
return defineRoute(name, {
|
|
235
234
|
...options,
|
|
236
235
|
path,
|
|
237
236
|
requirements: {
|
|
@@ -254,14 +253,14 @@ const Head = httpMethodDecorator(["HEAD"]);
|
|
|
254
253
|
* HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
|
|
255
254
|
* `Route.matchRequirements` ne lève jamais 405 sur la méthode.
|
|
256
255
|
*/
|
|
257
|
-
|
|
256
|
+
const All = function(path = "", options = {}) {
|
|
258
257
|
return function(target, propertyKey, descriptor) {
|
|
259
|
-
return
|
|
258
|
+
return defineRoute(`${target.constructor.name}::${propertyKey}`, {
|
|
260
259
|
...options,
|
|
261
260
|
path
|
|
262
261
|
})(target, propertyKey, descriptor);
|
|
263
262
|
};
|
|
264
|
-
}
|
|
263
|
+
};
|
|
265
264
|
/**
|
|
266
265
|
* Fixe le code de statut HTTP de la réponse d'une action.
|
|
267
266
|
*
|
|
@@ -312,7 +311,7 @@ function HttpCode(statusCode) {
|
|
|
312
311
|
*/
|
|
313
312
|
function Header(key, value) {
|
|
314
313
|
return function(target, propertyKey, descriptor) {
|
|
315
|
-
const existing = Reflect.getMetadata(
|
|
314
|
+
const existing = { ...Reflect.getMetadata(HEADERS_METADATA, target, propertyKey) };
|
|
316
315
|
existing[key] = value;
|
|
317
316
|
Reflect.defineMetadata(HEADERS_METADATA, existing, target, propertyKey);
|
|
318
317
|
return descriptor;
|
|
@@ -522,12 +521,12 @@ function IsGranted(attribute, options) {
|
|
|
522
521
|
};
|
|
523
522
|
return function(target, propertyKey, descriptor) {
|
|
524
523
|
if (propertyKey === void 0) {
|
|
525
|
-
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) ?? [];
|
|
524
|
+
const existing = [...Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) ?? []];
|
|
526
525
|
existing.push(clause);
|
|
527
526
|
Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target);
|
|
528
527
|
return target;
|
|
529
528
|
}
|
|
530
|
-
const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) ?? [];
|
|
529
|
+
const existing = [...Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) ?? []];
|
|
531
530
|
existing.push(clause);
|
|
532
531
|
Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target, propertyKey);
|
|
533
532
|
return descriptor;
|
|
@@ -578,12 +577,12 @@ function RequireScope(scope) {
|
|
|
578
577
|
const clause = { anyOf: Array.isArray(scope) ? [...scope] : [scope] };
|
|
579
578
|
return function(target, propertyKey, descriptor) {
|
|
580
579
|
if (propertyKey === void 0) {
|
|
581
|
-
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) ?? [];
|
|
580
|
+
const existing = [...Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) ?? []];
|
|
582
581
|
existing.push(clause);
|
|
583
582
|
Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target);
|
|
584
583
|
return target;
|
|
585
584
|
}
|
|
586
|
-
const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) ?? [];
|
|
585
|
+
const existing = [...Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) ?? []];
|
|
587
586
|
existing.push(clause);
|
|
588
587
|
Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target, propertyKey);
|
|
589
588
|
return descriptor;
|
|
@@ -700,7 +699,7 @@ function Idempotent(options) {
|
|
|
700
699
|
function paramDecoratorFactory(source) {
|
|
701
700
|
return function(key) {
|
|
702
701
|
return function(target, propertyKey, parameterIndex) {
|
|
703
|
-
const existing = Reflect.
|
|
702
|
+
const existing = Reflect.getOwnMetadata("route:paramArgs", target, propertyKey) ?? [];
|
|
704
703
|
existing.push({
|
|
705
704
|
source,
|
|
706
705
|
key,
|
|
@@ -721,26 +720,18 @@ const Query = paramDecoratorFactory("query");
|
|
|
721
720
|
*
|
|
722
721
|
* ## Le corps n'est PAS validé — le type écrit ici ne promet rien
|
|
723
722
|
*
|
|
724
|
-
* `@Body()` injecte le corps **tel qu'il a été
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
* taxerait toute requête à paramètres décorés).
|
|
729
|
-
*
|
|
730
|
-
* Où valider, donc :
|
|
723
|
+
* `@Body()` injecte le corps **tel qu'il a été décodé** (objet JSON ou de
|
|
724
|
+
* formulaire, chaîne pour `text/*`, octets `Uint8Array` pour tout autre type) :
|
|
725
|
+
* `@Body() dto: CreateOrder` compile, mais rien ne garantit qu'un `CreateOrder`
|
|
726
|
+
* soit arrivé.
|
|
731
727
|
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
|
|
737
|
-
* modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
|
|
738
|
-
* devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
|
|
739
|
-
* quel message, quelle règle.
|
|
728
|
+
* Pour un contrat vérifié, déclarer l'option de route `schema` (ADR-0018) : le
|
|
729
|
+
* corps est validé AVANT l'action — 422 qui nomme chaque champ —, sur HTTP comme
|
|
730
|
+
* sur le pont WebSocket, et l'action le reçoit typé dans `input.body`. Une action
|
|
731
|
+
* à schéma ne prend aucun décorateur de paramètre.
|
|
740
732
|
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
|
|
733
|
+
* Sans schéma, `schema.parse(body)` en première ligne de l'action reste le geste
|
|
734
|
+
* d'un cas isolé : une `ZodError` qui remonte devient le même **422**.
|
|
744
735
|
*
|
|
745
736
|
* @example
|
|
746
737
|
* ```ts
|
|
@@ -756,7 +747,7 @@ function Body(keyOrOptions) {
|
|
|
756
747
|
const key = isOptions ? void 0 : raw ?? void 0;
|
|
757
748
|
const stream = isOptions ? raw.stream === true : false;
|
|
758
749
|
return function(target, propertyKey, parameterIndex) {
|
|
759
|
-
const existing = Reflect.
|
|
750
|
+
const existing = Reflect.getOwnMetadata("route:paramArgs", target, propertyKey) ?? [];
|
|
760
751
|
const meta = {
|
|
761
752
|
source: "body",
|
|
762
753
|
key,
|
|
@@ -777,6 +768,75 @@ const Res = paramDecoratorFactory("res");
|
|
|
777
768
|
const UploadedFile = paramDecoratorFactory("file");
|
|
778
769
|
const UploadedFiles = paramDecoratorFactory("files");
|
|
779
770
|
/**
|
|
771
|
+
* La query de l'invocation : celle du chemin INVOQUÉ par le pont WebSocket
|
|
772
|
+
* (`queryOverride`), sinon celle de l'URL. Source UNIQUE de `@Query` et de
|
|
773
|
+
* `input.query` d'une route à schéma.
|
|
774
|
+
*/
|
|
775
|
+
function readQuery(ctx) {
|
|
776
|
+
return readQueryOf(ctx.queryOverride, ctx.request);
|
|
777
|
+
}
|
|
778
|
+
/** {@link readQuery}, depuis ses deux sources. */
|
|
779
|
+
function readQueryOf(queryOverride, request) {
|
|
780
|
+
return queryOverride ?? request?.query;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Le corps DÉCODÉ de l'invocation — source UNIQUE de `@Body` et de
|
|
784
|
+
* `input.body` d'une route à schéma, pour qu'un corps ne puisse pas prendre
|
|
785
|
+
* deux sens selon la forme de l'action.
|
|
786
|
+
*
|
|
787
|
+
* Pont `api.request` (WS) : le corps d'une mutation voyage dans l'ALS (aucun
|
|
788
|
+
* corps HTTP parsé sur une frame — posé par appel par
|
|
789
|
+
* `RealtimeController.invokeApiRequest`, zéro fuite entre appels) et PRIME sur
|
|
790
|
+
* `request.body` (vide en WS) — même priorité que
|
|
791
|
+
* `AdminApiController.buildRequest`. En HTTP, le magasin ALS ne porte jamais
|
|
792
|
+
* `body`. Aucune forme n'est supposée : un codec binaire y posera des octets.
|
|
793
|
+
*/
|
|
794
|
+
function readBody(ctx) {
|
|
795
|
+
return readInvocationBody(ctx.request);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Le corps DÉCODÉ d'une invocation, depuis la requête seule — la règle de
|
|
799
|
+
* {@link readBody}, pour qui n'a pas de contexte de paramètres (porte
|
|
800
|
+
* d'idempotence).
|
|
801
|
+
*
|
|
802
|
+
* @param request - la requête du contexte.
|
|
803
|
+
* @returns le corps posé par le pont WebSocket, sinon le corps HTTP lu.
|
|
804
|
+
*/
|
|
805
|
+
function readInvocationBody(request) {
|
|
806
|
+
const alsBody = RequestContext.get()?.body;
|
|
807
|
+
return alsBody !== void 0 ? alsBody : request?.body;
|
|
808
|
+
}
|
|
809
|
+
/** Un champ d'une valeur décodée ; `undefined` si elle n'est pas un objet. */
|
|
810
|
+
function readField(value, key) {
|
|
811
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
812
|
+
return Reflect.get(value, key);
|
|
813
|
+
}
|
|
814
|
+
/** Objet vide partagé : une partie absente n'alloue rien. Gelé, jamais muté. */
|
|
815
|
+
const EMPTY_PART = Object.freeze({});
|
|
816
|
+
/**
|
|
817
|
+
* L'entrée BRUTE d'une action à schéma, lue aux MÊMES sources que les
|
|
818
|
+
* décorateurs de paramètre ({@link readQuery}, {@link readBody}) : pont
|
|
819
|
+
* WebSocket compris. Seule allocation de la voie à schéma : cet objet, propre
|
|
820
|
+
* à l'invocation, que la validation complète en place et que l'action reçoit.
|
|
821
|
+
*
|
|
822
|
+
* Arguments POSITIONNELS, pas un objet de contexte : la voie à schéma n'en
|
|
823
|
+
* alloue aucun de plus.
|
|
824
|
+
*
|
|
825
|
+
* @param paramsMap - variables de route nommées.
|
|
826
|
+
* @param request - la requête du contexte.
|
|
827
|
+
* @param queryOverride - query du chemin invoqué (pont WebSocket), sinon rien.
|
|
828
|
+
* @param bodyStream - `true` : `body` est le flux brut, jamais lu.
|
|
829
|
+
* @returns les quatre parties, brutes.
|
|
830
|
+
*/
|
|
831
|
+
function readRouteInput(paramsMap, request, queryOverride, bodyStream) {
|
|
832
|
+
return {
|
|
833
|
+
params: paramsMap,
|
|
834
|
+
query: readQueryOf(queryOverride, request) ?? EMPTY_PART,
|
|
835
|
+
body: bodyStream ? request?.request : readInvocationBody(request),
|
|
836
|
+
headers: request?.headers ?? EMPTY_PART
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
780
840
|
* Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
|
|
781
841
|
*
|
|
782
842
|
* @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
|
|
@@ -787,15 +847,13 @@ function resolveParamArg(meta, ctx) {
|
|
|
787
847
|
switch (meta.source) {
|
|
788
848
|
case "param": return meta.key !== void 0 ? ctx.paramsMap[meta.key] : ctx.paramsMap;
|
|
789
849
|
case "query": {
|
|
790
|
-
const
|
|
791
|
-
return meta.key !== void 0 ?
|
|
850
|
+
const q = readQuery(ctx);
|
|
851
|
+
return meta.key !== void 0 ? q?.[meta.key] : q;
|
|
792
852
|
}
|
|
793
853
|
case "body": {
|
|
794
854
|
if (meta.stream) return ctx.request?.request;
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
const qp = ctx.request?.queryPost;
|
|
798
|
-
return meta.key !== void 0 ? qp?.[meta.key] : qp;
|
|
855
|
+
const body = readBody(ctx);
|
|
856
|
+
return meta.key !== void 0 ? readField(body, meta.key) : body;
|
|
799
857
|
}
|
|
800
858
|
case "headers": {
|
|
801
859
|
const h = ctx.request?.headers;
|
|
@@ -805,9 +863,9 @@ function resolveParamArg(meta, ctx) {
|
|
|
805
863
|
case "session": return meta.key !== void 0 ? ctx.session?.get(meta.key) : ctx.session;
|
|
806
864
|
case "req": return ctx.request;
|
|
807
865
|
case "res": return ctx.response;
|
|
808
|
-
case "file": return ctx.request?.
|
|
809
|
-
case "files": return ctx.request?.
|
|
810
|
-
case "user": return RequestContext.getUser();
|
|
866
|
+
case "file": return ctx.request?.files?.[0];
|
|
867
|
+
case "files": return ctx.request?.files;
|
|
868
|
+
case "user": return RequestContext.isAnonymous() ? void 0 : RequestContext.getUser();
|
|
811
869
|
default: return;
|
|
812
870
|
}
|
|
813
871
|
}
|
|
@@ -826,6 +884,87 @@ function buildParamArgs(metas, ctx) {
|
|
|
826
884
|
return result;
|
|
827
885
|
}
|
|
828
886
|
/**
|
|
887
|
+
* Résout les paramètres décorés d'une action depuis un `Context` — la mise en
|
|
888
|
+
* forme UNIQUE, appelée par le Resolver (routes HTTP et WS) comme par les
|
|
889
|
+
* actions `@RealtimeAction`, pour qu'un décorateur ne puisse pas prendre deux
|
|
890
|
+
* sens selon le transport.
|
|
891
|
+
*
|
|
892
|
+
* Le corps d'un appel WebSocket voyage dans l'ALS (`RequestContext` clé
|
|
893
|
+
* `body`) et `@CurrentUser` y lit l'utilisateur : l'appelant pose la bulle,
|
|
894
|
+
* cette fonction n'en crée aucune.
|
|
895
|
+
*
|
|
896
|
+
* @param metas - paramètres décorés de l'action ({@link getParamArgsMeta})
|
|
897
|
+
* @param context - le contexte de la requête ou de la connexion
|
|
898
|
+
* @param paramsMap - variables de route nommées (vide hors route)
|
|
899
|
+
* @param queryOverride - query de l'invocation quand elle ne vient pas de l'URL
|
|
900
|
+
* @returns arguments positionnels à passer à l'action
|
|
901
|
+
*/
|
|
902
|
+
function buildContextParamArgs(metas, context, paramsMap, queryOverride) {
|
|
903
|
+
return buildParamArgs(metas, {
|
|
904
|
+
paramsMap,
|
|
905
|
+
request: context.request,
|
|
906
|
+
response: context.response,
|
|
907
|
+
session: context.session,
|
|
908
|
+
queryOverride,
|
|
909
|
+
getRequestCookies: (name) => context.getRequestCookies(name)
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Les paramètres décorés d'une méthode (`@Body`, `@CurrentUser`…), ou `null`
|
|
914
|
+
* si elle n'en porte aucun. Lecture `Reflect` : à faire une fois, hors du
|
|
915
|
+
* chemin de chaque requête.
|
|
916
|
+
*
|
|
917
|
+
* @param proto - le prototype de la classe
|
|
918
|
+
* @param method - le nom de la méthode
|
|
919
|
+
* @returns les métadonnées, ou `null` — une action sans décorateur ne paie rien
|
|
920
|
+
*/
|
|
921
|
+
function getParamArgsMeta(proto, method) {
|
|
922
|
+
const metas = Reflect.getMetadata(PARAM_ARGS_METADATA, proto, method);
|
|
923
|
+
return metas !== void 0 && metas.length > 0 ? metas : null;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Le schéma d'entrée d'une méthode (option de route `schema`), ou `null`.
|
|
927
|
+
* Lecture `Reflect` : à faire une fois, hors du chemin de chaque requête.
|
|
928
|
+
*
|
|
929
|
+
* @param proto - le prototype de la classe.
|
|
930
|
+
* @param method - le nom de la méthode.
|
|
931
|
+
* @returns le schéma, ou `null` — une action sans schéma ne paie rien.
|
|
932
|
+
* @throws BootConfigurationError si le schéma rangé est mal formé.
|
|
933
|
+
*/
|
|
934
|
+
function getRouteSchema(proto, method) {
|
|
935
|
+
const schema = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
|
|
936
|
+
if (schema === void 0) return null;
|
|
937
|
+
assertRouteSchemaShape(schema, `${proto.constructor.name}.${method}`);
|
|
938
|
+
return schema;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Refuse, au DÉMARRAGE, une action à schéma mal déclarée — appelé par
|
|
942
|
+
* `Router.setController` pour chaque route du contrôleur, pour qu'une faute
|
|
943
|
+
* ne se révèle pas à la première requête :
|
|
944
|
+
*
|
|
945
|
+
* - schéma mal formé (partie inconnue, valeur qui n'est pas un Standard Schema) ;
|
|
946
|
+
* - schéma ET décorateurs de paramètre (`@Body`…) : une action à schéma reçoit
|
|
947
|
+
* un argument UNIQUE, les deux formes ne se combinent pas ;
|
|
948
|
+
* - `bodyStream: true` ET `schema.body` : un flux ne se valide pas.
|
|
949
|
+
*
|
|
950
|
+
* @param routeDef - la route (nom, contrôleur, méthode).
|
|
951
|
+
* @throws BootConfigurationError qui nomme la route.
|
|
952
|
+
*/
|
|
953
|
+
function assertRouteSchemaDeclaration(routeDef) {
|
|
954
|
+
const ctor = routeDef.controller;
|
|
955
|
+
const method = routeDef.classMethod;
|
|
956
|
+
if (!ctor || !method) return;
|
|
957
|
+
const proto = ctor.prototype;
|
|
958
|
+
const raw = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
|
|
959
|
+
if (raw === void 0) {
|
|
960
|
+
if (Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » exige un « schema » — sans schéma, demander le flux par @Body({ stream: true }).`);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
assertRouteSchemaShape(raw, routeDef.name);
|
|
964
|
+
if (getParamArgsMeta(proto, method) !== null) throw new BootConfigurationError(`Route "${routeDef.name}" : une action à schéma reçoit un argument unique (input) — retirer les décorateurs de paramètre (@Body, @Query, @Param…) de ${proto.constructor.name}.${method}, ou retirer l'option « schema ».`);
|
|
965
|
+
if (raw.body !== void 0 && Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » et « schema.body » s'excluent — un flux n'est pas lu, il ne peut pas être validé.`);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
829
968
|
* P2.9 — Indique si l'action d'une route attend le **flux brut** du body
|
|
830
969
|
* (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
|
|
831
970
|
* `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
|
|
@@ -840,18 +979,21 @@ function routeExpectsBodyStream(routeDef) {
|
|
|
840
979
|
let flag = false;
|
|
841
980
|
const ctor = routeDef.controller;
|
|
842
981
|
const method = routeDef.classMethod;
|
|
843
|
-
if (ctor && method) flag =
|
|
982
|
+
if (ctor && method) flag = Reflect.getMetadata(BODY_STREAM_METADATA, ctor.prototype, method) === true || (getParamArgsMeta(ctor.prototype, method)?.some((m) => m.source === "body" && m.stream === true) ?? false);
|
|
844
983
|
routeDef.bodyStream = flag;
|
|
845
984
|
}
|
|
846
985
|
return routeDef.bodyStream;
|
|
847
986
|
}
|
|
848
987
|
const EMPTY_ACTION_META = {
|
|
849
988
|
paramsMeta: null,
|
|
989
|
+
schema: null,
|
|
990
|
+
bodyStream: false,
|
|
850
991
|
redirectMeta: null,
|
|
851
992
|
httpCode: null,
|
|
852
993
|
headerEntries: null,
|
|
853
994
|
sessionIntent: null,
|
|
854
995
|
security: null,
|
|
996
|
+
authentication: null,
|
|
855
997
|
cspDirectives: null,
|
|
856
998
|
csrfProtect: false,
|
|
857
999
|
csrfExempt: false,
|
|
@@ -942,17 +1084,19 @@ function computeIdempotent(ctor, method) {
|
|
|
942
1084
|
function computeActionMeta(ctor, method) {
|
|
943
1085
|
if (!ctor || !method) return EMPTY_ACTION_META;
|
|
944
1086
|
const proto = ctor.prototype;
|
|
945
|
-
const params = Reflect.getMetadata(PARAM_ARGS_METADATA, proto, method);
|
|
946
1087
|
const redirect = Reflect.getMetadata(REDIRECT_METADATA, proto, method);
|
|
947
1088
|
const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA, proto, method);
|
|
948
1089
|
const headers = Reflect.getMetadata(HEADERS_METADATA, proto, method);
|
|
949
1090
|
return {
|
|
950
|
-
paramsMeta:
|
|
1091
|
+
paramsMeta: getParamArgsMeta(proto, method),
|
|
1092
|
+
schema: getRouteSchema(proto, method),
|
|
1093
|
+
bodyStream: Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true,
|
|
951
1094
|
redirectMeta: redirect ?? null,
|
|
952
1095
|
httpCode: httpCode ?? null,
|
|
953
1096
|
headerEntries: headers ? Object.entries(headers) : null,
|
|
954
1097
|
sessionIntent: resolveSessionIntent(ctor, method),
|
|
955
1098
|
security: computeSecurityRequirement(ctor, method),
|
|
1099
|
+
authentication: computeAuthenticationRequirement(ctor, method),
|
|
956
1100
|
cspDirectives: computeCspDirectives(ctor, method),
|
|
957
1101
|
csrfProtect: Reflect.getMetadata(CSRF_PROTECT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_PROTECT_METADATA, ctor) === true,
|
|
958
1102
|
csrfExempt: Reflect.getMetadata(CSRF_EXEMPT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_EXEMPT_METADATA, ctor) === true,
|
|
@@ -972,5 +1116,64 @@ function resolveActionMeta(routeDef) {
|
|
|
972
1116
|
routeDef.actionMeta ??= computeActionMeta(routeDef.controller, routeDef.classMethod);
|
|
973
1117
|
return routeDef.actionMeta;
|
|
974
1118
|
}
|
|
975
|
-
|
|
976
|
-
|
|
1119
|
+
const AUTHENTICATION_REQUIREMENT_METADATA = "nodefony:security:authentication";
|
|
1120
|
+
/**
|
|
1121
|
+
* Exige une connexion RÉCENTE et/ou faite avec certains moyens pour l'action
|
|
1122
|
+
* (décorateur de méthode) ou tout le contrôleur (décorateur de classe).
|
|
1123
|
+
*
|
|
1124
|
+
* - `@RequireAuthentication({ maxAge: 300 })` — connexion de moins de 5 minutes.
|
|
1125
|
+
* - `@RequireAuthentication({ amr: ["otp"] })` — second facteur exigé.
|
|
1126
|
+
* - les deux se combinent ; classe + méthode fusionnent au plus strict.
|
|
1127
|
+
*
|
|
1128
|
+
* Lit ce que la session a retenu à son ouverture (`readSessionAuthentication`,
|
|
1129
|
+
* ADR-0015) et s'évalue dans le `Resolver` APRÈS `@IsGranted` : un rôle absent
|
|
1130
|
+
* reste un 403 définitif, une connexion trop ancienne ou trop faible devient un
|
|
1131
|
+
* **401** dont le `WWW-Authenticate` porte
|
|
1132
|
+
* `error="insufficient_user_authentication"` (RFC 9470) — le client sait qu'il
|
|
1133
|
+
* doit redemander une connexion, pas que l'accès lui est fermé. Une session qui
|
|
1134
|
+
* ne porte pas ces métadonnées est refusée, jamais acceptée.
|
|
1135
|
+
*
|
|
1136
|
+
* @param options - âge maximal et moyens exigés.
|
|
1137
|
+
* @throws TypeError si `maxAge` n'est pas un entier positif ou si un moyen est vide.
|
|
1138
|
+
*/
|
|
1139
|
+
function RequireAuthentication(options = {}) {
|
|
1140
|
+
const { maxAge, amr = [] } = options;
|
|
1141
|
+
if (maxAge !== void 0 && (!Number.isInteger(maxAge) || maxAge <= 0)) throw new TypeError(`@RequireAuthentication : maxAge doit être un entier de secondes > 0, reçu ${String(maxAge)}`);
|
|
1142
|
+
if (amr.some((m) => typeof m !== "string" || m.length === 0)) throw new TypeError("@RequireAuthentication : chaque moyen de `amr` est une chaîne non vide (RFC 8176 : pwd, otp, pop)");
|
|
1143
|
+
const requirement = Object.freeze({
|
|
1144
|
+
maxAge: maxAge ?? null,
|
|
1145
|
+
amr: Object.freeze([...amr])
|
|
1146
|
+
});
|
|
1147
|
+
return function(target, propertyKey, descriptor) {
|
|
1148
|
+
if (propertyKey === void 0) {
|
|
1149
|
+
Reflect.defineMetadata(AUTHENTICATION_REQUIREMENT_METADATA, requirement, target);
|
|
1150
|
+
return target;
|
|
1151
|
+
}
|
|
1152
|
+
Reflect.defineMetadata(AUTHENTICATION_REQUIREMENT_METADATA, requirement, target, propertyKey);
|
|
1153
|
+
return descriptor;
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/** Une métadonnée lue par `Reflect` qui a bien la forme d'une exigence, sinon `undefined`. */
|
|
1157
|
+
function asAuthenticationRequirement(value) {
|
|
1158
|
+
return typeof value === "object" && value !== null && "maxAge" in value && "amr" in value && (value.maxAge === null || typeof value.maxAge === "number") && Array.isArray(value.amr) ? {
|
|
1159
|
+
maxAge: value.maxAge,
|
|
1160
|
+
amr: value.amr.filter((m) => typeof m === "string")
|
|
1161
|
+
} : void 0;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Fusionne les `@RequireAuthentication` de classe et de méthode au plus strict,
|
|
1165
|
+
* ou `null` si aucune — `@Anonymous` (méthode) rend l'action publique.
|
|
1166
|
+
*/
|
|
1167
|
+
function computeAuthenticationRequirement(ctor, method) {
|
|
1168
|
+
if (Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor, method) === true) return null;
|
|
1169
|
+
const onMethod = asAuthenticationRequirement(Reflect.getMetadata(AUTHENTICATION_REQUIREMENT_METADATA, ctor.prototype, method));
|
|
1170
|
+
const onClass = Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor) === true ? void 0 : asAuthenticationRequirement(Reflect.getMetadata(AUTHENTICATION_REQUIREMENT_METADATA, ctor));
|
|
1171
|
+
if (onMethod === void 0) return onClass ?? null;
|
|
1172
|
+
if (onClass === void 0) return onMethod;
|
|
1173
|
+
const ages = [onClass.maxAge, onMethod.maxAge].filter((age) => age !== null);
|
|
1174
|
+
return Object.freeze({
|
|
1175
|
+
maxAge: ages.length > 0 ? Math.min(...ages) : null,
|
|
1176
|
+
amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
3
2
|
import router_default from "./router.js";
|
|
4
3
|
import AdminApiController from "../controller/AdminApiController.js";
|
|
5
|
-
import { ADMIN_DEFAULT_ROLE,
|
|
6
|
-
|
|
4
|
+
import { ADMIN_DEFAULT_ROLE, Service, injectable, resolveAdminRole } from "nodefony";
|
|
5
|
+
// nodefony/service/AdminBroker.ts
|
|
7
6
|
const serviceName = "adminBroker";
|
|
8
7
|
/**
|
|
9
8
|
* Implémentation du data plane admin (Studio) — collecte les {@link IAdminApi}
|
|
@@ -101,7 +100,6 @@ let AdminBroker = class AdminBroker extends Service {
|
|
|
101
100
|
return this.get("router");
|
|
102
101
|
}
|
|
103
102
|
};
|
|
104
|
-
AdminBroker = __decorate([injectable()
|
|
103
|
+
AdminBroker = __decorate([injectable()], AdminBroker);
|
|
105
104
|
var AdminBroker_default = AdminBroker;
|
|
106
|
-
//#endregion
|
|
107
105
|
export { AdminBroker_default as default };
|
|
@@ -2,7 +2,7 @@ import Template from "../src/Template.js";
|
|
|
2
2
|
import { extend, injectable } from "nodefony";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { Eta as Eta$1 } from "eta";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/Eta.ts
|
|
6
6
|
/**
|
|
7
7
|
* Options par défaut du moteur Eta.
|
|
8
8
|
*
|
|
@@ -64,5 +64,4 @@ var Eta = class extends Template {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
//#endregion
|
|
68
67
|
export { Eta as default };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
//#region nodefony/service/IdempotencyStore.ts
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
2
|
+
import { Service, assertPageQuery, injectable } from "nodefony";
|
|
3
|
+
// nodefony/service/IdempotencyStore.ts
|
|
5
4
|
const serviceName = "idempotencyStore";
|
|
6
5
|
/** Réponses mémorisées : 10 min (un rejeu plausible reste dans cette fenêtre). */
|
|
7
6
|
const DEFAULT_TTL_MS = 6e5;
|
|
@@ -129,7 +128,6 @@ let MemoryIdempotencyStore = class MemoryIdempotencyStore extends Service {
|
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
};
|
|
132
|
-
MemoryIdempotencyStore = __decorate([injectable()
|
|
131
|
+
MemoryIdempotencyStore = __decorate([injectable()], MemoryIdempotencyStore);
|
|
133
132
|
var IdempotencyStore_default = MemoryIdempotencyStore;
|
|
134
|
-
//#endregion
|
|
135
133
|
export { IdempotencyStore_default as default };
|