@nodefony/framework 10.0.0-beta.6 → 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 (38) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/nodefony/controller/CspReportController.js +2 -2
  5. package/dist/nodefony/controller/IssuerMetadataController.js +2 -2
  6. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +74 -9
  7. package/dist/nodefony/controller/SessionAuthController.js +1 -1
  8. package/dist/nodefony/decorators/routerDecorators.js +101 -13
  9. package/dist/nodefony/grpc/GrpcController.js +138 -0
  10. package/dist/nodefony/grpc/GrpcService.js +75 -0
  11. package/dist/nodefony/grpc/declarations.js +33 -0
  12. package/dist/nodefony/grpc/index.js +3 -0
  13. package/dist/nodefony/service/AdminBroker.js +1 -1
  14. package/dist/nodefony/service/IdempotencyStore.js +1 -1
  15. package/dist/nodefony/service/router.js +11 -9
  16. package/dist/nodefony/src/Controller.js +21 -3
  17. package/dist/nodefony/src/Resolver.js +83 -43
  18. package/dist/nodefony/src/Route.js +45 -45
  19. package/dist/nodefony/src/loginPage.js +1 -1
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
  22. package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
  23. package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
  24. package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
  25. package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
  26. package/dist/types/nodefony/grpc/index.d.ts +13 -0
  27. package/dist/types/nodefony/interfaces/IController.d.ts +2 -2
  28. package/dist/types/nodefony/service/router.d.ts +5 -4
  29. package/dist/types/nodefony/src/Controller.d.ts +15 -3
  30. package/dist/types/nodefony/src/Resolver.d.ts +34 -7
  31. package/dist/types/nodefony/src/Route.d.ts +35 -0
  32. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  33. package/docs/controller.md +89 -9
  34. package/docs/grpc.md +690 -0
  35. package/docs/idempotence.md +11 -6
  36. package/docs/index.md +12 -8
  37. package/docs/routing.md +30 -2
  38. package/package.json +24 -8
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.157.0/helpers/esm/decorate.js
1
+ // \0@oxc-project+runtime@0.158.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.157.0/helpers/esm/decorateParam.js
1
+ // \0@oxc-project+runtime@0.158.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { RedisIdempotencyStore } from "./nodefony/src/RedisIdempotencyStore.js";
5
5
  import { scheduleIdempotencyGc } from "./nodefony/src/idempotencyGc.js";
6
6
  import router_default, { FRAMEWORK_ROUTER, isFrameworkRouter } from "./nodefony/service/router.js";
7
7
  import Route from "./nodefony/src/Route.js";
8
- import Controller, { configureFileServing } from "./nodefony/src/Controller.js";
8
+ import Controller, { configureFileServing, contentDisposition } from "./nodefony/src/Controller.js";
9
9
  import ResourceController from "./nodefony/src/ResourceController.js";
10
10
  import Resolver from "./nodefony/src/Resolver.js";
11
11
  import AdminBroker_default from "./nodefony/service/AdminBroker.js";
@@ -28,7 +28,7 @@ import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
28
28
  import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
29
29
  import Eta from "./nodefony/service/Eta.js";
30
30
  import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, controller, controllers, getParamArgsMeta, getRouteSchema, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
31
- import __decorate from "./_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
31
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
32
32
  import { validateRouteInput } from "./nodefony/src/routeSchema.js";
33
33
  import path from "node:path";
34
34
  import { AUTO_STORE, EMPTY_INFRA, Module, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, services, storeConnectivity } from "nodefony";
@@ -208,4 +208,4 @@ Framework = __decorate([services([
208
208
  IdempotencyStore_default
209
209
  ])], Framework);
210
210
  var framework_default = Framework;
211
- export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CspReportController, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, FRAMEWORK_ROUTER, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, getRouteSchema, isFrameworkRouter, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountCspReportRoute, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream, validateRouteInput };
211
+ export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CspReportController, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, FRAMEWORK_ROUTER, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, contentDisposition, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, getRouteSchema, isFrameworkRouter, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountCspReportRoute, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream, validateRouteInput };
@@ -1,8 +1,8 @@
1
1
  import router_default from "../service/router.js";
2
2
  import Controller from "../src/Controller.js";
3
3
  import { Body, CsrfExempt } from "../decorators/routerDecorators.js";
4
- import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
5
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
4
+ import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
6
6
  import { CSP_REPORT_MAX_BYTES, CSP_REPORT_PATH } from "nodefony";
7
7
  import { HttpError } from "@nodefony/http";
8
8
  // nodefony/controller/CspReportController.ts
@@ -92,7 +92,7 @@ var IssuerMetadataController = class extends Controller {
92
92
  try {
93
93
  document = buildAuthorizationServerMetadata({ issuer });
94
94
  } catch (error) {
95
- this.log(`métadonnées d'émetteur impubliables : ${error.message}`, "CRITIC");
95
+ this.log(`métadonnées d'émetteur impubliables : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
96
96
  return this.renderJson({ error: "server_error" }, 500);
97
97
  }
98
98
  return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
@@ -110,7 +110,7 @@ var IssuerMetadataController = class extends Controller {
110
110
  try {
111
111
  keys = await publisher.getPublicJWKS();
112
112
  } catch (error) {
113
- this.log(`JWKS illisible : ${error.message}`, "CRITIC");
113
+ this.log(`JWKS illisible : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
114
114
  return this.renderJson({ error: "server_error" }, 500);
115
115
  }
116
116
  return this.renderJson(keys, 200, { "Cache-Control": "public, max-age=300" });
@@ -66,9 +66,10 @@ var ProtectedResourceMetadataController = class extends Controller {
66
66
  /** Chemin bien connu demandé, tel que le Router l'a matché. */
67
67
  #askedPath() {
68
68
  const req = this.request;
69
- const p = req?.pathname;
69
+ if (typeof req !== "object" || req === null) return null;
70
+ const p = "pathname" in req ? req.pathname : void 0;
70
71
  if (typeof p === "string") return p;
71
- const url = req?.url;
72
+ const url = "url" in req ? req.url : void 0;
72
73
  if (url instanceof URL) return url.pathname;
73
74
  if (typeof url === "string") try {
74
75
  return new URL(url, "http://localhost").pathname;
@@ -112,7 +113,7 @@ var ProtectedResourceMetadataController = class extends Controller {
112
113
  try {
113
114
  document = buildProtectedResourceMetadata(match);
114
115
  } catch (error) {
115
- this.log(`métadonnées de ressource protégée impubliables : ${error.message}`, "CRITIC");
116
+ this.log(`métadonnées de ressource protégée impubliables : ${error instanceof Error ? error.message : String(error)}`, "CRITIC");
116
117
  return this.renderJson({ error: "server_error" }, 500);
117
118
  }
118
119
  return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
@@ -140,23 +141,87 @@ var ProtectedResourceMetadataController = class extends Controller {
140
141
  * interface à implémenter formellement — le même couplage que `tokenService` ou
141
142
  * `adminBroker`.
142
143
  *
144
+ * Deux sources peuvent déclarer la MÊME ressource — une zone du pare-feu et la
145
+ * porte MCP qu'elle couvre, typiquement — : elles sont fusionnées
146
+ * ({@link mergeProtectedResources}), jamais départagées par leur ordre.
147
+ *
143
148
  * @param container - conteneur de services de l'application
144
- * @returns les ressources déclarées, dans l'ordre des services
149
+ * @returns les ressources déclarées, une entrée par ressource, dans l'ordre des
150
+ * services
145
151
  */
146
152
  function collectProtectedResources(container) {
147
153
  const collected = [];
148
154
  for (const name of container.keys()) {
149
155
  const service = container.get(name);
150
- if (typeof service?.publishedProtectedResources !== "function") continue;
156
+ if (typeof service !== "object" || service === null || !("publishedProtectedResources" in service) || typeof service.publishedProtectedResources !== "function") continue;
151
157
  let declared;
152
158
  try {
153
- declared = service.publishedProtectedResources();
159
+ declared = Reflect.apply(service.publishedProtectedResources, service, []);
160
+ } catch {
161
+ continue;
162
+ }
163
+ if (!Array.isArray(declared)) continue;
164
+ for (const entry of declared) if (isProtectedResourceInput(entry)) collected.push(entry);
165
+ }
166
+ return mergeProtectedResources(collected);
167
+ }
168
+ /** Une liste de chaînes, ou rien. */
169
+ function isStringList(value) {
170
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
171
+ }
172
+ /**
173
+ * Une déclaration a-t-elle la forme d'une ressource publiable ? Le contrat est
174
+ * structurel ({@link IProtectedResourcePublisher}) : ce qu'un service rend se
175
+ * VÉRIFIE, il ne se suppose pas.
176
+ */
177
+ function isProtectedResourceInput(value) {
178
+ if (typeof value !== "object" || value === null) return false;
179
+ if (!("resource" in value) || typeof value.resource !== "string") return false;
180
+ if (!("authorizationServers" in value) || !isStringList(value.authorizationServers)) return false;
181
+ return !("scopesSupported" in value) || value.scopesSupported === void 0 || isStringList(value.scopesSupported);
182
+ }
183
+ /** L'union de deux listes, dans l'ordre d'apparition. */
184
+ function union(first, second) {
185
+ return [.../* @__PURE__ */ new Set([...first, ...second])];
186
+ }
187
+ /**
188
+ * Une ressource, UN document : fusionne les déclarations d'une même ressource
189
+ * (même URI canonique) — l'union de leurs serveurs d'autorisation et de leurs
190
+ * scopes ; le premier nom et la première documentation déclarés.
191
+ *
192
+ * Pourquoi fusionner plutôt que garder la première : une zone du pare-feu
193
+ * publie sa ressource avec les émetteurs de confiance, et la porte MCP qu'elle
194
+ * couvre publie la MÊME ressource avec les scopes de ses outils. Garder la
195
+ * première jetait les scopes sans un mot — et un agent qui suit le document
196
+ * demandait alors un jeton sans eux, qui n'ouvrait pas l'outil.
197
+ *
198
+ * @param entries - déclarations, dans l'ordre des services
199
+ * @returns une entrée par ressource, à la place de la première déclaration
200
+ */
201
+ function mergeProtectedResources(entries) {
202
+ const merged = [];
203
+ const byResource = /* @__PURE__ */ new Map();
204
+ for (const entry of entries) {
205
+ let key;
206
+ try {
207
+ key = canonicalResourceUri(entry.resource);
154
208
  } catch {
209
+ merged.push(entry);
210
+ continue;
211
+ }
212
+ const seen = byResource.get(key);
213
+ if (seen === void 0) {
214
+ const first = { ...entry };
215
+ byResource.set(key, first);
216
+ merged.push(first);
155
217
  continue;
156
218
  }
157
- for (const entry of declared) collected.push(entry);
219
+ seen.authorizationServers = union(seen.authorizationServers, entry.authorizationServers);
220
+ if (entry.scopesSupported !== void 0) seen.scopesSupported = union(seen.scopesSupported ?? [], entry.scopesSupported);
221
+ if (seen.resourceName === void 0 && entry.resourceName !== void 0) seen.resourceName = entry.resourceName;
222
+ if (seen.resourceDocumentation === void 0 && entry.resourceDocumentation !== void 0) seen.resourceDocumentation = entry.resourceDocumentation;
158
223
  }
159
- return collected;
224
+ return merged;
160
225
  }
161
226
  /**
162
227
  * Chemins bien connus à monter pour un jeu de ressources déclarées.
@@ -217,4 +282,4 @@ function mountProtectedResourceRoutes(frameworkModule, resources) {
217
282
  mounted = true;
218
283
  return paths.length;
219
284
  }
220
- export { collectProtectedResources, ProtectedResourceMetadataController as default, mountProtectedResourceRoutes, protectedResourceRoutePaths };
285
+ export { collectProtectedResources, ProtectedResourceMetadataController as default, mergeProtectedResources, mountProtectedResourceRoutes, protectedResourceRoutePaths };
@@ -1,7 +1,7 @@
1
1
  import { CsrfProtect } from "../decorators/routerDecorators.js";
2
2
  import router_default from "../service/router.js";
3
3
  import SecurityApiController from "../src/SecurityApiController.js";
4
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
4
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
5
5
  import { AUTH_API_BASE } from "nodefony";
6
6
  // nodefony/controller/SessionAuthController.ts
7
7
  let mounted = false;
@@ -97,6 +97,36 @@ function controllers(targets) {
97
97
  };
98
98
  }
99
99
  /**
100
+ * Applique à une route en cours de montage ce que la classe et la méthode
101
+ * déclarent par décorateur : `@Domain` (hôte) et `@BypassFirewall`. Seule
102
+ * implémentation de ces précédences — `@controller` et `@GrpcService` montent
103
+ * leurs routes par elle.
104
+ *
105
+ * Précédences : `@route({ host })` > `@Domain` méthode > `@Domain` classe ;
106
+ * option `bypassFirewall: true` > `@BypassFirewall` méthode > classe. Lecture
107
+ * au montage, fail-closed : un oubli laisse la route gardée.
108
+ *
109
+ * @param ctor - la classe contrôleur.
110
+ * @param options - les options de la route, complétées EN PLACE.
111
+ * @param method - la méthode dont les décorateurs s'appliquent (celle du
112
+ * contrat pour une route gRPC, pas l'action de base).
113
+ *
114
+ * @internal
115
+ */
116
+ function applyDeclaredRouteOptions(ctor, options, method) {
117
+ if (options.host === void 0) {
118
+ const methodDomain = method === void 0 ? void 0 : Reflect.getMetadata(DOMAIN_METHOD_METADATA, ctor, method);
119
+ const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, ctor);
120
+ const domain = methodDomain ?? classDomain;
121
+ if (domain) options.host = domain;
122
+ }
123
+ if (options.bypassFirewall !== true) {
124
+ const methodBypass = method === void 0 ? void 0 : Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, ctor, method);
125
+ const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, ctor);
126
+ if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
127
+ }
128
+ }
129
+ /**
100
130
  * Declaration Controller
101
131
  *
102
132
  * @param prefix - prefixage du router du controller.
@@ -124,17 +154,7 @@ function controller(prefix) {
124
154
  let hasMagic = false;
125
155
  for (const [name, options] of Object.entries(metadata)) {
126
156
  options.prefix = prefix;
127
- if (options.host === void 0) {
128
- const methodDomain = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(DOMAIN_METHOD_METADATA, mycontroller, options.classMethod);
129
- const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, mycontroller);
130
- const domain = methodDomain ?? classDomain;
131
- if (domain) options.host = domain;
132
- }
133
- if (options.bypassFirewall !== true) {
134
- const methodBypass = options.classMethod === void 0 ? void 0 : Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, mycontroller, options.classMethod);
135
- const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
136
- if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
137
- }
157
+ applyDeclaredRouteOptions(mycontroller, options, options.classMethod);
138
158
  if (options.path === "*") {
139
159
  hasMagic = {
140
160
  options,
@@ -153,6 +173,72 @@ function controller(prefix) {
153
173
  function route(name, options) {
154
174
  return defineRoute(name, options);
155
175
  }
176
+ /** Plus long délai qu'un minuteur Node honore : au-delà, il tombe à 1 ms. */
177
+ const MAX_TIMER_MS = 2147483647;
178
+ /**
179
+ * Valide l'option de route `responseTimeout` À LA DÉCLARATION : un entier de
180
+ * 0 à 2³¹−1. Hors bornes, Node ramènerait le minuteur à 1 ms — chaque requête
181
+ * de la route expirerait aussitôt, sans que rien ne dise pourquoi.
182
+ *
183
+ * @param value - l'option telle que déclarée (absente = celle du serveur).
184
+ * @param className - classe du contrôleur, nommée dans l'erreur.
185
+ * @param method - action, nommée dans l'erreur.
186
+ * @returns la valeur validée, ou `undefined` si l'option est absente.
187
+ * @throws BootConfigurationError qui nomme l'action et la borne.
188
+ */
189
+ function checkedResponseTimeout(value, className, method) {
190
+ if (value === void 0) return void 0;
191
+ if (!Number.isInteger(value) || value < 0 || value > MAX_TIMER_MS) throw new BootConfigurationError(`${className}.${method} : « responseTimeout » entier entre 0 et ${MAX_TIMER_MS} ms attendu (reçu ${String(value)}).`);
192
+ return value;
193
+ }
194
+ /** Les bornes d'envoi chiffrées qu'une route peut régler pour elle-même. */
195
+ const UPLOAD_LIMIT_KEYS = /* @__PURE__ */ new Set([
196
+ "maxFileSize",
197
+ "maxTotalFileSize",
198
+ "maxFiles",
199
+ "maxFields",
200
+ "maxFieldsSize"
201
+ ]);
202
+ function isUploadLimitKey(key) {
203
+ return UPLOAD_LIMIT_KEYS.has(key);
204
+ }
205
+ /** Les empreintes que le flux d'envoi sait calculer. */
206
+ function isUploadHash(value) {
207
+ return value === false || value === "sha1" || value === "md5" || value === "sha256";
208
+ }
209
+ /**
210
+ * Valide l'option de route `upload` À LA DÉCLARATION : des entiers ≥ 1 pour
211
+ * les bornes, une empreinte connue, aucune clé inconnue. Une faute de frappe
212
+ * (`maxFilesize`) laisserait sinon la route sur la borne de l'application —
213
+ * 500 Mo par défaut — sans que rien ne le dise.
214
+ *
215
+ * @param value - l'option telle que déclarée (absente = celles de la config).
216
+ * @param bodyStream - la route laisse son corps en flux : ses bornes ne
217
+ * s'appliqueraient jamais, la déclaration est contradictoire.
218
+ * @param className - classe du contrôleur, nommée dans l'erreur.
219
+ * @param method - action, nommée dans l'erreur.
220
+ * @returns les bornes validées, figées, sans clé `undefined` (la fusion sur
221
+ * la configuration ne doit rien écraser par du vide), ou `undefined`.
222
+ * @throws BootConfigurationError qui nomme l'action et la clé fautive.
223
+ */
224
+ function checkedUpload(value, bodyStream, className, method) {
225
+ if (value === void 0) return void 0;
226
+ const where = `${className}.${method} : « upload »`;
227
+ if (bodyStream) throw new BootConfigurationError(`${where} et « bodyStream: true » s'excluent — un corps laissé en flux n'est jamais lu par le pipeline, ses bornes ne s'appliqueraient pas.`);
228
+ const checked = {};
229
+ for (const [key, entry] of Object.entries(value)) {
230
+ if (entry === void 0) continue;
231
+ if (key === "hashAlgorithm") {
232
+ if (!isUploadHash(entry)) throw new BootConfigurationError(`${where} : « hashAlgorithm » vaut false, "sha1", "md5" ou "sha256" (reçu ${String(entry)}).`);
233
+ checked.hashAlgorithm = entry;
234
+ continue;
235
+ }
236
+ if (!isUploadLimitKey(key)) throw new BootConfigurationError(`${where} : clé inconnue « ${key} » — une route règle ${[...UPLOAD_LIMIT_KEYS].join(", ")} et hashAlgorithm ; le dossier et l'encodage restent ceux de la configuration.`);
237
+ if (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 1) throw new BootConfigurationError(`${where} : « ${key} » entier ≥ 1 attendu (reçu ${String(entry)}).`);
238
+ checked[key] = entry;
239
+ }
240
+ return Object.freeze(checked);
241
+ }
156
242
  /** L'implémentation de {@link route}, sans les surcharges de typage. */
157
243
  function defineRoute(name, options) {
158
244
  return function(target, propertyKey, descriptor) {
@@ -190,7 +276,9 @@ function defineRoute(name, options) {
190
276
  defaults: options.defaults,
191
277
  requirements: options.requirements,
192
278
  bypassFirewall: options.bypassFirewall,
193
- fallback: options.fallback
279
+ fallback: options.fallback,
280
+ responseTimeout: checkedResponseTimeout(options.responseTimeout, className, propertyKey),
281
+ upload: checkedUpload(options.upload, options.bodyStream === true, className, propertyKey)
194
282
  };
195
283
  Reflect.defineMetadata(metadataKey, metadata, target.constructor);
196
284
  return descriptor;
@@ -1176,4 +1264,4 @@ function computeAuthenticationRequirement(ctor, method) {
1176
1264
  amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
1177
1265
  });
1178
1266
  }
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 };
1267
+ 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, applyDeclaredRouteOptions, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getReservedActionNames, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
@@ -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
1
  import router_default from "./router.js";
2
2
  import AdminApiController from "../controller/AdminApiController.js";
3
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.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.157.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";