@nodefony/framework 10.0.0-beta.5 → 10.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
  3. package/dist/index.js +14 -10
  4. package/dist/nodefony/config/config.js +16 -2
  5. package/dist/nodefony/controller/AdminApiController.js +18 -5
  6. package/dist/nodefony/controller/BenchController.js +1 -1
  7. package/dist/nodefony/controller/CspReportController.js +70 -0
  8. package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
  9. package/dist/nodefony/controller/LoginPageController.js +5 -7
  10. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
  11. package/dist/nodefony/controller/SessionAuthController.js +6 -7
  12. package/dist/nodefony/controller/TokenAuthController.js +3 -3
  13. package/dist/nodefony/decorators/routerDecorators.js +103 -15
  14. package/dist/nodefony/grpc/GrpcController.js +138 -0
  15. package/dist/nodefony/grpc/GrpcService.js +75 -0
  16. package/dist/nodefony/grpc/declarations.js +33 -0
  17. package/dist/nodefony/grpc/index.js +3 -0
  18. package/dist/nodefony/service/AdminBroker.js +1 -1
  19. package/dist/nodefony/service/IdempotencyStore.js +1 -1
  20. package/dist/nodefony/service/router.js +15 -12
  21. package/dist/nodefony/src/Controller.js +330 -115
  22. package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
  23. package/dist/nodefony/src/Resolver.js +85 -51
  24. package/dist/nodefony/src/Route.js +45 -45
  25. package/dist/nodefony/src/SecurityApiController.js +30 -7
  26. package/dist/nodefony/src/conditionalRequest.js +103 -0
  27. package/dist/nodefony/src/docsReader.js +1 -1
  28. package/dist/nodefony/src/idempotency.js +1 -1
  29. package/dist/nodefony/src/loginPage.js +3 -2
  30. package/dist/nodefony/src/scopeCatalog.js +1 -1
  31. package/dist/nodefony/src/viewNonce.js +58 -0
  32. package/dist/types/index.d.ts +4 -2
  33. package/dist/types/nodefony/config/config.d.ts +20 -0
  34. package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
  35. package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
  36. package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
  37. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
  38. package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
  39. package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
  40. package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
  41. package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
  42. package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
  43. package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
  44. package/dist/types/nodefony/grpc/index.d.ts +13 -0
  45. package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
  46. package/dist/types/nodefony/service/router.d.ts +8 -6
  47. package/dist/types/nodefony/src/Controller.d.ts +124 -14
  48. package/dist/types/nodefony/src/Resolver.d.ts +35 -8
  49. package/dist/types/nodefony/src/Route.d.ts +35 -0
  50. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
  51. package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
  52. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  53. package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
  54. package/docs/controller.md +234 -35
  55. package/docs/decorateurs.md +1 -1
  56. package/docs/grpc.md +690 -0
  57. package/docs/idempotence.md +13 -8
  58. package/docs/index.md +16 -10
  59. package/docs/routing.md +30 -2
  60. package/docs/templates.md +15 -0
  61. package/package.json +28 -11
@@ -0,0 +1,138 @@
1
+ import Controller from "../src/Controller.js";
2
+ import { computeActionMeta, getReservedActionNames } from "../decorators/routerDecorators.js";
3
+ import { grpcDeclarationOf } from "./declarations.js";
4
+ import { BootConfigurationError } from "nodefony";
5
+ import { HttpContext } from "@nodefony/http";
6
+ import { GrpcEndpoint, markGrpcRoute } from "@nodefony/http/grpc";
7
+ // nodefony/grpc/GrpcController.ts
8
+ /**
9
+ * Nom de l'action de base, celle que toute route gRPC exécute : elle remet la
10
+ * requête à l'adaptateur. Les gardes, elles, se lisent sur la méthode du
11
+ * contrat.
12
+ *
13
+ * @internal
14
+ */
15
+ const GRPC_ACTION = "serveGrpc";
16
+ /**
17
+ * Ce que chaque décorateur sans effet en gRPC laisse dans la méta d'action —
18
+ * un refus au démarrage le nomme (ADR-0026 D1, point 5 ; S-grpc-18).
19
+ */
20
+ function inertDecorators(meta) {
21
+ const found = [];
22
+ if (meta.paramsMeta !== null) found.push("@Body/@Param/@Query… (paramètres)");
23
+ if (meta.schema !== null) found.push("schéma de route");
24
+ if (meta.sessionIntent !== null) found.push("@UseSession");
25
+ if (meta.cspDirectives !== null) found.push("@Csp");
26
+ if (meta.idempotent !== null) found.push("@Idempotent");
27
+ if (meta.httpCode !== null) found.push("@HttpCode");
28
+ if (meta.headerEntries !== null) found.push("@Header");
29
+ if (meta.redirectMeta !== null) found.push("@Redirect");
30
+ return found;
31
+ }
32
+ /**
33
+ * Classe de base d'un point d'entrée gRPC (ADR-0026 D1), jumelle de
34
+ * `RealtimeController` : marquée par `@GrpcService(Contrat)`, déclarée par
35
+ * `@controllers([...])`. Chaque méthode du contrat est une méthode de la
36
+ * classe, appelée par l'adaptateur Connect avec `(request, HandlerContext)` ;
37
+ * la logique métier vit dans un service injecté.
38
+ *
39
+ * Singleton par défaut : aucune instance par appel, l'adaptateur lie les
40
+ * méthodes une fois. `@Scope("request")` donne une instance par appel.
41
+ *
42
+ * Ce que le framework décide pour chaque méthode, sans code de ta part :
43
+ * - les gardes (`@IsGranted`, `@RequireAuthentication`…) posées sur la
44
+ * méthode ou la classe s'appliquent ; un refus part en 16
45
+ * `UNAUTHENTICATED` sans identité, en 7 `PERMISSION_DENIED` sinon ;
46
+ * - `this.user`, l'ALS et la portée `request` valent pendant tout l'appel,
47
+ * flux compris ;
48
+ * - une `ConnectError` part telle quelle ; une `HttpError` est traduite par
49
+ * la table de `google/rpc/code.proto` ; toute autre exception devient 13
50
+ * `INTERNAL`. Dans un flux, la traduction ne vaut qu'avant le premier
51
+ * message ;
52
+ * - un flux est interrompu en 14 `UNAVAILABLE` à l'arrêt du serveur et en 16
53
+ * à la révocation de son identité : passer `ctx.signal` à ses sources ;
54
+ * - messages bornés à 4 Mio (section `grpc` de la configuration http).
55
+ *
56
+ * Un décorateur sans effet en gRPC (`@Body`, `@UseSession`, `@Csp`,
57
+ * `@Idempotent`…) sur une méthode du contrat interrompt le démarrage.
58
+ * Guide complet : `docs/grpc.md` du paquet.
59
+ *
60
+ * @example
61
+ * \@GrpcService(GreetService)
62
+ * export class GreetController extends GrpcController {
63
+ * constructor(context: ContextType) {
64
+ * super("GreetController", context);
65
+ * }
66
+ * greet(request: GreetRequest) {
67
+ * return { greeting: `Bonjour, ${request.name} !` };
68
+ * }
69
+ * }
70
+ *
71
+ * @experimental Surface gRPC, ADR-0026 D11.
72
+ */
73
+ var GrpcController = class extends Controller {
74
+ /**
75
+ * Vérifie la déclaration gRPC de la classe et fige ses routes — au
76
+ * démarrage (`Router.setController`), jamais à la première requête.
77
+ *
78
+ * Refuse : une classe sans `@GrpcService` ; une méthode du contrat absente
79
+ * de la classe ou dont le nom est réservé par le framework ; un décorateur
80
+ * sans effet en gRPC sur une méthode du contrat (ou sur la classe) — le
81
+ * refus nomme la méthode et le décorateur.
82
+ *
83
+ * Fige ensuite, sur chaque route, la méta d'action lue sur la MÉTHODE DU
84
+ * CONTRAT (l'action exécutée reste celle de la base) et le corps laissé en
85
+ * flux : les gardes valent quel que soit l'ordre des décorateurs de classe.
86
+ *
87
+ * @throws BootConfigurationError sur une déclaration refusée.
88
+ */
89
+ static assertScope() {
90
+ const declaration = grpcDeclarationOf(this);
91
+ if (declaration === void 0) throw new BootConfigurationError(`Contrôleur gRPC « ${this.name} » sans @GrpcService(Contrat) : rien ne dit quel service il sert. Poser @GrpcService sur la classe.`);
92
+ const typeName = declaration.service.typeName;
93
+ const reserved = getReservedActionNames();
94
+ const metas = /* @__PURE__ */ new Map();
95
+ for (const method of declaration.service.methods) {
96
+ const name = method.localName;
97
+ const where = `Service gRPC ${typeName} (« ${this.name} »), méthode « ${name} »`;
98
+ const owner = name === "serveGrpc" || name === "grpcEndpoint" ? "GrpcController" : reserved.get(name);
99
+ if (owner !== void 0) throw new BootConfigurationError(`${where} : ce nom est réservé par ${owner}, dont tout contrôleur hérite. Renommer la méthode dans le contrat.`);
100
+ if (typeof Reflect.get(this.prototype, name) !== "function") throw new BootConfigurationError(`${where} : absente de la classe. Chaque méthode du contrat s'implémente sur son contrôleur.`);
101
+ const meta = computeActionMeta(this, name);
102
+ const inert = inertDecorators(meta);
103
+ if (inert.length !== 0) throw new BootConfigurationError(`${where} : ${inert.join(", ")} sans effet sur un appel gRPC (posé sur la méthode ou sur la classe). Les retirer.`);
104
+ metas.set(name, meta);
105
+ }
106
+ for (const { route, method } of declaration.routes) {
107
+ const meta = metas.get(method.localName);
108
+ if (meta === void 0) continue;
109
+ route.actionMeta = {
110
+ ...meta,
111
+ bodyStream: true
112
+ };
113
+ route.bodyStream = true;
114
+ markGrpcRoute(route);
115
+ }
116
+ Object.defineProperty(this.prototype, "grpcEndpoint", {
117
+ value: new GrpcEndpoint(declaration.service, { perCall: this.scope !== "singleton" }),
118
+ configurable: true
119
+ });
120
+ }
121
+ /**
122
+ * L'action de base de toute route gRPC : remet la requête et la réponse
123
+ * brutes à l'adaptateur, qui appelle la méthode du contrat et écrit la
124
+ * réponse. Synchrone, sauf pour un flux dont la session est à sauver : la
125
+ * promesse de la remise est alors rendue au résolveur, qui en lit l'échec.
126
+ *
127
+ * @throws Error quand la classe n'a pas été enregistrée par le routeur, ou
128
+ * hors d'une requête HTTP.
129
+ */
130
+ serveGrpc() {
131
+ const endpoint = this.grpcEndpoint;
132
+ if (endpoint === void 0) throw new Error(`Contrôleur gRPC « ${this.constructor.name} » non enregistré : le déclarer par @controllers([...]).`);
133
+ const context = this.context;
134
+ if (!(context instanceof HttpContext)) throw new Error("Un appel gRPC n'est servi que sur une requête HTTP.");
135
+ return endpoint.serve(context, this);
136
+ }
137
+ };
138
+ export { GRPC_ACTION, GrpcController };
@@ -0,0 +1,75 @@
1
+ import router_default from "../service/router.js";
2
+ import { applyDeclaredRouteOptions } from "../decorators/routerDecorators.js";
3
+ import { GRPC_ACTION } from "./GrpcController.js";
4
+ import { claimGrpcService, declareGrpcService } from "./declarations.js";
5
+ import { BootConfigurationError } from "nodefony";
6
+ import { MethodOptions_IdempotencyLevel } from "@bufbuild/protobuf/wkt";
7
+ // nodefony/grpc/GrpcService.ts
8
+ /**
9
+ * Déclare un contrôleur comme le point d'entrée du service gRPC `service`
10
+ * (le descripteur généré par `buf`, ex. `EchoService` de `echo_pb.ts`).
11
+ *
12
+ * Monte une route littérale du Router par méthode du contrat,
13
+ * `POST /<paquet.Service>/<Méthode>`, plus `GET` pour une méthode déclarée
14
+ * `option idempotency_level = NO_SIDE_EFFECTS` (le GET de Connect). Ces
15
+ * routes passent par tout le pipeline — pare-feu, `@IsGranted`, hôtes
16
+ * virtuels, profileur, liste des routes —, et leurs gardes se lisent sur la
17
+ * méthode du contrat. La classe se déclare ensuite par `@controllers([...])`,
18
+ * comme tout point d'entrée d'un module.
19
+ *
20
+ * Les décorateurs de classe `@Domain` et `@BypassFirewall` se placent SOUS
21
+ * `@GrpcService` (lus au montage des routes, comme sous `@controller`).
22
+ *
23
+ * Le descripteur vient du code généré, jamais écrit à la main : contrat dans
24
+ * `proto/<paquet>/v1/*.proto`, `npx buf generate` vers `nodefony/gen/`, code
25
+ * commité. Les routes répondent sur 5152 (gRPC, gRPC-Web, Connect), 5151
26
+ * (gRPC-Web, Connect : HTTP/1.1, jamais le gRPC natif) et 5153 si h2c est
27
+ * activé. Guide complet : `docs/grpc.md` du paquet.
28
+ *
29
+ * @param service - le descripteur du service.
30
+ * @returns le décorateur de classe.
31
+ * @throws BootConfigurationError quand une autre classe sert déjà ce contrat.
32
+ *
33
+ * @example
34
+ * \@GrpcService(EchoService)
35
+ * export class EchoController extends GrpcController {
36
+ * constructor(context: ContextType) {
37
+ * super("EchoController", context);
38
+ * }
39
+ * \@IsGranted("ROLE_ECHO")
40
+ * say(request: SayRequest) {
41
+ * return { sentence: request.sentence };
42
+ * }
43
+ * }
44
+ *
45
+ * @experimental Surface gRPC, ADR-0026 D11.
46
+ */
47
+ function GrpcService(service) {
48
+ return function(ctor) {
49
+ const owner = claimGrpcService(service.typeName, ctor.name);
50
+ if (owner !== null) throw new BootConfigurationError(`@GrpcService(${service.typeName}) sur « ${ctor.name} » : ce service est déjà servi par « ${owner} ». Un contrat n'a qu'un point d'entrée.`);
51
+ const routes = [];
52
+ for (const method of service.methods) {
53
+ const options = {
54
+ prefix: `/${service.typeName}`,
55
+ path: `/${method.name}`,
56
+ constructor: ctor,
57
+ className: ctor.name,
58
+ classMethod: GRPC_ACTION,
59
+ method: method.idempotency === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS ? ["POST", "GET"] : "POST"
60
+ };
61
+ applyDeclaredRouteOptions(ctor, options, method.localName);
62
+ const route = router_default.createRoute(`${ctor.name}::${method.localName}`, options);
63
+ routes.push({
64
+ route,
65
+ method
66
+ });
67
+ }
68
+ declareGrpcService(ctor, {
69
+ service,
70
+ routes
71
+ });
72
+ return ctor;
73
+ };
74
+ }
75
+ export { GrpcService };
@@ -0,0 +1,33 @@
1
+ // nodefony/grpc/declarations.ts
2
+ /**
3
+ * Déclarations par classe contrôleur. Écrite à l'import (`@GrpcService`), lue
4
+ * une fois au démarrage (`GrpcController.assertScope`) : jamais sur le chemin
5
+ * d'un appel.
6
+ */
7
+ const declarations = /* @__PURE__ */ new WeakMap();
8
+ /**
9
+ * Contrat déjà servi, par nom complet : deux classes qui serviraient le même
10
+ * service monteraient deux fois les mêmes chemins.
11
+ */
12
+ const servedBy = /* @__PURE__ */ new Map();
13
+ /** @internal */
14
+ function declareGrpcService(ctor, declaration) {
15
+ declarations.set(ctor, declaration);
16
+ }
17
+ /** @internal */
18
+ function grpcDeclarationOf(ctor) {
19
+ return declarations.get(ctor);
20
+ }
21
+ /**
22
+ * Retient quelle classe sert un contrat.
23
+ *
24
+ * @returns le nom de la classe qui le servait déjà, ou `null`.
25
+ * @internal
26
+ */
27
+ function claimGrpcService(typeName, className) {
28
+ const owner = servedBy.get(typeName);
29
+ if (owner !== void 0) return owner;
30
+ servedBy.set(typeName, className);
31
+ return null;
32
+ }
33
+ export { claimGrpcService, declareGrpcService, grpcDeclarationOf };
@@ -0,0 +1,3 @@
1
+ import { GrpcController } from "./GrpcController.js";
2
+ import { GrpcService } from "./GrpcService.js";
3
+ export { GrpcController, GrpcService };
@@ -1,6 +1,6 @@
1
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
2
1
  import router_default from "./router.js";
3
2
  import AdminApiController from "../controller/AdminApiController.js";
3
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
4
4
  import { ADMIN_DEFAULT_ROLE, Service, injectable, resolveAdminRole } from "nodefony";
5
5
  // nodefony/service/AdminBroker.ts
6
6
  const serviceName = "adminBroker";
@@ -1,4 +1,4 @@
1
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
1
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
2
2
  import { Service, assertPageQuery, injectable } from "nodefony";
3
3
  // nodefony/service/IdempotencyStore.ts
4
4
  const serviceName = "idempotencyStore";
@@ -1,7 +1,7 @@
1
1
  import Route from "../src/Route.js";
2
- import { assertRouteSchemaDeclaration, routeExpectsBodyStream } from "../decorators/routerDecorators.js";
3
2
  import Resolver from "../src/Resolver.js";
4
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
+ import { assertRouteSchemaDeclaration, routeExpectsBodyStream } from "../decorators/routerDecorators.js";
4
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
5
5
  import { BootConfigurationError, Injector, Module, Service, injectable, isPromise, stripTrailingSlashes } from "nodefony";
6
6
  import { HttpError, isDomainAllowed } from "@nodefony/http";
7
7
  // nodefony/service/router.ts
@@ -173,10 +173,11 @@ let Router = class Router extends Service {
173
173
  * lieu de `context.request.url` — permet de router un path **porté par un message**
174
174
  * (WS-RPC `invoke`) vers une action, sans muter l'URL de la connexion (état partagé).
175
175
  * `undefined` (cas hot path normal) → comportement inchangé.
176
- * @param methodOverride - méthode HTTP **logique** à exiger en plus du transport
177
- * WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
178
- * GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
179
- * `undefined` (GET/HTTP) → match historique sur `context.method`.
176
+ * @param methodOverride - méthode HTTP **logique** du pont WS-RPC `api.request`,
177
+ * GET compris, exigée en plus du transport WEBSOCKET par la règle du cœur
178
+ * `bridgeServesVerb` : un GET n'atteint jamais une action POST du même
179
+ * chemin. `undefined` (requête HTTP, ouverture de socket) → match sur
180
+ * `context.method`.
180
181
  * @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
181
182
  * pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
182
183
  * `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
@@ -220,8 +221,9 @@ let Router = class Router extends Service {
220
221
  return resolver;
221
222
  }
222
223
  } catch (e) {
223
- this.log(`Match route exception : ${route.name} ${String(e)}`, "DEBUG");
224
- resolver.exception = e;
224
+ routeMatchLogged ??= this.kernel?.environment !== "production";
225
+ if (routeMatchLogged) this.log(`Match route exception : ${route.name} ${String(e)}`, "DEBUG");
226
+ resolver.exception = e instanceof Error ? e : new Error(String(e));
225
227
  continue;
226
228
  }
227
229
  }
@@ -239,12 +241,12 @@ let Router = class Router extends Service {
239
241
  Upgrade: "websocket",
240
242
  Connection: "Upgrade"
241
243
  });
242
- resolver.exception = new HttpError("Upgrade Required: websocket", 426, context);
244
+ resolver.exception = HttpError.withoutStack("Upgrade Required: websocket", 426, context);
243
245
  return resolver;
244
246
  }
245
247
  if (allowed.size > 0) {
246
248
  const allowHeader = Array.from(allowed).join(", ");
247
- const err = new HttpError(`Method ${context.method} Not Allowed`, 405, context);
249
+ const err = HttpError.withoutStack(`Method ${context.method} Not Allowed`, 405, context);
248
250
  const methodErr = err;
249
251
  methodErr.allow = allowHeader;
250
252
  methodErr.type = "method";
@@ -349,11 +351,12 @@ Router = _Router = __decorate([injectable()], Router);
349
351
  * installées du framework feraient échouer `instanceof` en silence. Une
350
352
  * lecture de propriété.
351
353
  *
352
- * @param router - le routeur lu sur le contexte (`context.router`).
354
+ * @param router - le routeur lu sur le contexte (`context.router`) ou le
355
+ * service `router` du conteneur.
353
356
  * @returns `true` si c'est le Router du framework.
354
357
  */
355
358
  function isFrameworkRouter(router) {
356
- return router != null && FRAMEWORK_ROUTER in router;
359
+ return typeof router === "object" && router !== null && FRAMEWORK_ROUTER in router;
357
360
  }
358
361
  var router_default = Router;
359
362
  export { FRAMEWORK_ROUTER, router_default as default, isFrameworkRouter };