@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
@@ -263,7 +263,7 @@ Deux mécanismes rendent ça possible côté socket :
263
263
  `resolveIdempotencyKey()` (`idempotency.ts#resolveIdempotencyKey`) donne la **priorité à l'ALS** sur l'en-tête HTTP.
264
264
  - **La méthode logique voyage par `methodOverride`** — sur une socket, `context.method` vaut
265
265
  `WEBSOCKET`, qui n'est **pas** une mutation. Sans override, la porte serait sautée et un rejeu de
266
- frame `socket.mutate` créerait un doublon. Le Resolver teste donc
266
+ frame `socket.post` créerait un doublon. Le Resolver teste donc
267
267
  `isMutationMethod(this.methodOverride ?? context.method)` (`Resolver.ts#Resolver.action`).
268
268
 
269
269
  Le pont utilise `executeActionGuarded()` (`Resolver.ts#Resolver.executeActionGuarded`) : porte d'idempotence **sans** rendu HTTP
@@ -304,16 +304,21 @@ sequenceDiagram
304
304
  comparaison, flux normal, **zéro** lookup de store et zéro allocation.
305
305
  2. **No-op sur méthode sûre.** Une action `GET` sous une classe `@Idempotent` repart directement en
306
306
  exécution (`Resolver.ts#Resolver._callWithIdempotency`).
307
- 3. **Empreinte du payload.** `computeFingerprint()` (`idempotency.ts#computeFingerprint`) hache
308
- `[nom de route, params de route, corps]` (`Resolver.ts#Resolver._enforceActionSecurity`). Le corps vient de l'ALS (pont WS) ou
307
+ 3. **La garde d'abord.** `@IsGranted`, `@RequireScope` et le rôle de la zone sont jugés AVANT
308
+ la porte (`Resolver.ts#Resolver._enforceActionSecurity`), comme avant la validation d'un schéma :
309
+ un appelant sans le droit reçoit 401/403, jamais un 400 « clé requise » ni un 422 qui lui
310
+ apprendrait le contrat de la route ; aucune clé n'est réservée pour lui ; et un **rejeu est
311
+ rejugé** — un droit retiré entre-temps rend 403, pas la réponse mémorisée.
312
+ 4. **Empreinte du payload.** `computeFingerprint()` (`idempotency.ts#computeFingerprint`) hache
313
+ `[nom de route, params de route, corps]` (`Resolver.ts#Resolver._callWithIdempotency`). Le corps vient de l'ALS (pont WS) ou
309
314
  du body HTTP parsé.
310
- 4. **Identité.** `resolveIdentity()` (`idempotency.ts#resolveIdentity`) rend l'`identifier` de l'utilisateur
315
+ 5. **Identité.** `resolveIdentity()` (`idempotency.ts#resolveIdentity`) rend l'`identifier` de l'utilisateur
311
316
  AUTHENTIFIÉ de la requête (`RequestContext.getAuthenticatedUser()`, la règle unique du cœur).
312
317
  L'anonyme d'une zone `anonymous` n'a pas d'identité : tous les visiteurs anonymes partagent le
313
318
  même `identifier`, ils partageraient donc le même cache. **`null` = pas de cache** : le verdict
314
319
  devient `execute` — jamais de partage cross-identité.
315
- 5. **Réservation.** `store.begin()` compose la clé scopée et tranche.
316
- 6. **Mémorisation.** En succès, `complete(clé, {status, body})` où `status` est le code de réponse
320
+ 6. **Réservation.** `store.begin()` compose la clé scopée et tranche.
321
+ 7. **Mémorisation.** En succès, `complete(clé, {status, body})` où `status` est le code de réponse
317
322
  courant et `body` la **valeur retournée** par l'action (`Resolver.ts#Resolver._callWithIdempotency`). En erreur,
318
323
  `abort(clé)` libère la clé : **un échec ne se mémorise pas**, il doit rester réessayable.
319
324
 
@@ -323,7 +328,7 @@ sequenceDiagram
323
328
  > double-effet reste évité, mais le corps rejoué est vide. Pire, si le corps retourné n'est pas
324
329
  > sérialisable (retour d'un objet Response circulaire), un store SQL/Redis lève au `stringify`. Le
325
330
  > Resolver attrape ce cas : il **journalise un WARNING explicite** puis mémorise le statut avec un
326
- > corps `null` (`Resolver.ts#Resolver._acquireAndInvoke`) — la dédup est préservée plutôt que perdue en silence.
331
+ > corps `null` (`Resolver.ts#Resolver._callWithIdempotency`) — la dédup est préservée plutôt que perdue en silence.
327
332
 
328
333
  ### Où la porte est câblée
329
334
 
@@ -656,7 +661,7 @@ pour l'affichage Studio, `idempotencyStoreRegistry.ts#listIdempotencyBackends`).
656
661
  <!-- prettier-ignore -->
657
662
  | Sujet | Norme | Ancrage |
658
663
  | --- | --- | --- |
659
- | En-tête `Idempotency-Key`, statuts, rejeu | `draft-ietf-httpapi-idempotency-key-header-06` | `evaluateIdempotency()` (`idempotency.ts#evaluateIdempotency`) |
664
+ | En-tête `Idempotency-Key`, statuts, rejeu | `draft-ietf-httpapi-idempotency-key-header-08` | `evaluateIdempotency()` (`idempotency.ts#evaluateIdempotency`) |
660
665
  | Clé réutilisée avec un autre payload → 422 | draft §2.2 / §2.7 | `idempotency.ts#evaluateIdempotency` |
661
666
  | Exécution concurrente identique → 409 | draft §2.6 | `idempotency.ts#evaluateIdempotency` |
662
667
  | Clé requise absente → 400 | draft §2.7 | `idempotency.ts#evaluateIdempotency` |
package/docs/index.md CHANGED
@@ -53,14 +53,15 @@ Trois parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppo
53
53
 
54
54
  Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouve.
55
55
 
56
- | Brique | Ce qu'elle résout | Tu en as besoin quand… |
57
- | ------------------------------- | -------------------------------------------------- | --------------------------------------------------------- |
58
- | [Décorateurs](decorateurs.md) | déclarer routes, paramètres, réponses, gardes | toujours — c'est la surface d'écriture |
59
- | [Contrôleurs](controller.md) | recevoir la requête, répondre, gérer l'erreur | toujours |
60
- | [Routage](routing.md) | apparier une URL à une action, arbitrer, expliquer | deux routes se disputent, ou un 404/405 surprend |
61
- | [Idempotence](idempotence.md) | empêcher le double effet d'une mutation rejouée | paiement, commande, tout effet non rejouable |
62
- | [Admin (data plane)](admin.md) | monter les API d'admin `/nodefony/<ns>/api/*` | ton module expose des stats ou actions à Studio et au CLI |
63
- | [Templates (Eta)](templates.md) | rendre des vues HTML côté serveur | tu renvoies des pages HTML plutôt que du JSON |
56
+ | Brique | Ce qu'elle résout | Tu en as besoin quand… |
57
+ | ------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------- |
58
+ | [Décorateurs](decorateurs.md) | déclarer routes, paramètres, réponses, gardes | toujours — c'est la surface d'écriture |
59
+ | [Contrôleurs](controller.md) | recevoir la requête, répondre, gérer l'erreur | toujours |
60
+ | [Routage](routing.md) | apparier une URL à une action, arbitrer, expliquer | deux routes se disputent, ou un 404/405 surprend |
61
+ | [Idempotence](idempotence.md) | empêcher le double effet d'une mutation rejouée | paiement, commande, tout effet non rejouable |
62
+ | [gRPC](grpc.md) | servir un service gRPC, gRPC-Web et Connect | des programmes d'autres langages, des sondes ou un navigateur appellent un contrat typé |
63
+ | [Admin (data plane)](admin.md) | monter les API d'admin `/nodefony/<ns>/api/*` | ton module expose des stats ou actions à Studio et au CLI |
64
+ | [Templates (Eta)](templates.md) | rendre des vues HTML côté serveur | tu renvoies des pages HTML plutôt que du JSON |
64
65
 
65
66
  ```nodefony-cards
66
67
  [
@@ -76,6 +77,9 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
76
77
  { "icon": "🔁", "title": "idempotence", "href": "idempotence.md",
77
78
  "desc": "@Idempotent, la clé d'idempotence, les quatre stores (mémoire, Redis, SQL, MongoDB) et leurs capacités réelles, le GC des entrées expirées, et ce que le client observe quand il rejoue la même clé.",
78
79
  "meta": "paiement, commande, tout effet non rejouable" },
80
+ { "icon": "📡", "title": "grpc", "href": "grpc.md",
81
+ "desc": "Déclarer un service gRPC par un contrôleur : le contrat et sa génération, les trois protocoles et leurs ports, les limites, les refus 16/7, les erreurs, l'arrêt, CORS et le journal — avec le pourquoi de chaque choix. Surface expérimentale.",
82
+ "meta": "un contrat typé appelé par d'autres langages, des sondes ou un navigateur" },
79
83
  { "icon": "🛡️", "title": "admin", "href": "admin.md",
80
84
  "desc": "Le data plane d'administration : comment un module déclare son API d'admin via AdminBroker, la convention de route /nodefony/<ns>/api/*, le RBAC fail-closed (ROLE_NODEFONY_ADMIN), le duplex HTTP/WebSocket, le catalogue et le Playground.",
81
85
  "meta": "exposer une API d'admin cohérente CLI ↔ Web" },
@@ -108,8 +112,10 @@ dans `.ai/symbols.json` et les types générés — jamais recopiées ici, où e
108
112
  ## ⚙️ Configuration
109
113
 
110
114
  Bloc Zod (`nodefony/config/config.ts`), déclaré depuis l'application via
111
- `use("@nodefony/framework", { … })` : `router`, `adminBroker`, et `idempotency` (choix du store et
112
- réglages du GC — détaillé dans la page [Idempotence](idempotence.md)).
115
+ `use("@nodefony/framework", { … })` : `router`, `adminBroker`, `idempotency` (choix du store et
116
+ réglages du GC — détaillé dans la page [Idempotence](idempotence.md)), et `fileServing` (validateurs
117
+ et `Cache-Control` des fichiers servis par une action — détaillé dans la page
118
+ [Contrôleur](controller.md#requêtes-conditionnelles--304-412-reprise)).
113
119
 
114
120
  ## 📜 Normes appliquées
115
121
 
package/docs/routing.md CHANGED
@@ -457,11 +457,14 @@ que fait le data plane d'administration pour toutes ses lectures (`AdminBroker.m
457
457
  (RFC 9110 §10.2.1), elle est retirée de l'agrégat. Un chemin qui n'existe qu'en WebSocket, appelé
458
458
  en HTTP, répond `426 Upgrade Required` avec `Upgrade: websocket` plutôt qu'un 405
459
459
  (`routing-nonregression.test.ts`).
460
- - **Une invocation WS d'une mutation doit dire quelle méthode logique elle vise.** Sur une socket,
460
+ - **Une invocation WS dit toujours quelle méthode logique elle vise, GET compris.** Sur une socket,
461
461
  `context.method` vaut toujours `WEBSOCKET` : insuffisant pour distinguer un GET d'un POST sur le même
462
462
  chemin. Le pont transporte donc une **méthode logique** (`methodOverride`, `Resolver.ts#Resolver.methodOverride`) que la
463
463
  route doit déclarer **en plus** du transport — une route `POST` qui n'annonce pas `WEBSOCKET` reste
464
- **injoignable** par socket (zéro contournement, `Route.ts#Route.matchRequirements`).
464
+ **injoignable** par socket, et un GET n'atteint jamais l'action POST voisine (qui contournerait sa
465
+ porte `@Idempotent`). Seule tolérance : une route qui ne déclare QUE `WEBSOCKET` sert un GET. La
466
+ règle est UNIQUE, au cœur (`routePath.ts#bridgeServesVerb`) : le routeur
467
+ (`Route.ts#Route.matchRequirements`) et la vérification des contrats au démarrage l'appellent tous deux.
465
468
 
466
469
  Le routage par **message** (invoquer un chemin porté par une frame, sans toucher l'URL de la connexion)
467
470
  passe par le même `resolve()`, avec un chemin fourni en argument — l'état partagé de la socket n'est
@@ -538,6 +541,31 @@ chemin ; pour un appel interne, utilise `forward()`.
538
541
  dans le **même** contexte de requête, sans repasser par le réseau (`Resolver.parsePathernController()`,
539
542
  `Resolver.ts#Resolver.parsePathernController`). Une vraie redirection HTTP passe par `this.redirect(url, 302)` ou `@Redirect`.
540
543
 
544
+ ## ⏱️ Délai de réponse — par route
545
+
546
+ Une action muette au-delà du délai de réponse reçoit **503**, en HTTP/1.1 comme en HTTP/2, et son
547
+ `ctx.signal` est annulé AVANT le rendu de l'erreur : une requête SQL ou un `fetch` qui l'honore
548
+ s'arrête. Le délai est celui du serveur (`responseTimeout`, 30 s par défaut), sauf si la route
549
+ déclare le sien (`Route.ts#RouteOptions.responseTimeout`) — dans les deux sens :
550
+
551
+ ```ts ignore
552
+ @Get("/export", { responseTimeout: 120_000 }) // un export lent, sans relever les autres routes
553
+ exportAll(): unknown {
554
+ return this.reports.buildAll();
555
+ }
556
+ ```
557
+
558
+ Les bornes d'envoi suivent le même modèle : `upload: { maxFileSize, maxFiles, hashAlgorithm, … }`
559
+ (`Route.ts#RouteOptions.upload`) remplace, pour la seule route, celles de la configuration http —
560
+ le fichier trop gros reçoit 413 pendant son flux. Détail : [Upload](../../http/docs/upload.md#par-route--loption-upload).
561
+
562
+ C'est un délai d'**inactivité** : une action qui envoie au fil de l'eau (flux, SSE) n'est pas
563
+ coupée. Entier de `0` à 2³¹−1 ms, vérifié à la déclaration ; `0` désactive le délai pour la route.
564
+ Le 408 reste réservé à une requête que le serveur n'a pas **reçue** à temps (`requestTimeout`,
565
+ RFC 9110 §15.5.9) ; le 503 dit « traitement impossible dans le délai » (§15.6.4), sans inviter le
566
+ client à rejouer un `POST`. Une route gRPC termine l'appel au format du protocole (4
567
+ `DEADLINE_EXCEEDED`), jamais par ce 503.
568
+
541
569
  ## 🧰 API publique
542
570
 
543
571
  Le routage s'utilise **par décorateurs** ; l'API impérative sert l'outillage (introspection, tests,
package/docs/templates.md CHANGED
@@ -249,6 +249,21 @@ inerte `&lt;script&gt;alert(1)&lt;/script&gt;`.
249
249
  > par un utilisateur (pseudo, commentaire, champ de formulaire, paramètre d'URL). Réserve-le à des
250
250
  > fragments HTML que ton propre code a construits.
251
251
 
252
+ **La politique de sécurité du contenu (CSP) est signée pour toi.** La politique par défaut n'exécute
253
+ un `<script>` et n'applique un `<style>` que s'ils portent le nonce de la requête. `renderView` le
254
+ pose seul sur chaque balise ÉCRITE dans le gabarit (`viewNonce.ts#signTemplateTags`) — rien à
255
+ ajouter. Il ne signe jamais :
256
+
257
+ - une balise que les données apportent (`<%~ %>`) : c'est ce qui laisse au nonce sa valeur contre
258
+ l'injection ;
259
+ - un `<script>` dont un attribut vient des données (`src="<%= it.url %>"`) : le signer exécuterait
260
+ l'URL qu'elles apportent. Si tu la tiens pour sûre, écris le nonce toi-même :
261
+ `<script nonce="<%= it.cspNonce %>" src="<%= it.url %>">` ;
262
+ - un gestionnaire en attribut (`onclick="…"`, `onload="…"`) : aucun nonce ne s'y pose, la politique
263
+ le refuse. Attache l'écouteur depuis un `<script>` du gabarit (`addEventListener`).
264
+
265
+ Le détail de la politique et ses réglages : [En-têtes de sécurité](../../security/docs/headers.md).
266
+
252
267
  ## 🧰 API publique
253
268
 
254
269
  Deux niveaux : ce que le **service Eta** expose, et ce que le **contrôleur** t'offre au-dessus.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/framework",
3
- "version": "10.0.0-beta.5",
3
+ "version": "10.0.0-beta.7",
4
4
  "description": "Le modèle de programmation Nodefony : routeur, contrôleurs, décorateurs et vues — HTTP et WebSocket dans le même contexte",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,12 @@
12
12
  "types": "./dist/types/index.d.ts",
13
13
  "import": "./dist/index.js",
14
14
  "default": "./dist/index.js"
15
+ },
16
+ "./grpc": {
17
+ "nodefony-source": "./nodefony/grpc/index.ts",
18
+ "types": "./dist/types/nodefony/grpc/index.d.ts",
19
+ "import": "./dist/nodefony/grpc/index.js",
20
+ "default": "./dist/nodefony/grpc/index.js"
15
21
  }
16
22
  },
17
23
  "scripts": {
@@ -25,7 +31,7 @@
25
31
  },
26
32
  "private": false,
27
33
  "engines": {
28
- "node": ">=24.14.0"
34
+ "node": "^24.16.0 || >=26.0.0"
29
35
  },
30
36
  "keywords": [
31
37
  "nodefony",
@@ -46,25 +52,36 @@
46
52
  "dependencies": {
47
53
  "eta": "4.6.0",
48
54
  "reflect-metadata": "0.2.2",
49
- "tslib": "2.8.1"
55
+ "tslib": "2.9.1"
50
56
  },
51
57
  "devDependencies": {
52
- "@nodefony/http": "^10.0.0-beta.5",
58
+ "@nodefony/http": "^10.0.0-beta.7",
53
59
  "@types/chai": "5.2.3",
54
- "@types/node": "26.6.5",
55
- "@vitest/coverage-v8": "5.0.5",
56
- "nodefony": "^10.0.0-beta.5",
60
+ "@types/node": "26.6.6",
61
+ "@vitest/coverage-v8": "5.1.0",
62
+ "nodefony": "^10.0.0-beta.7",
57
63
  "tsx": "4.23.15",
58
- "vitest": "5.0.5"
64
+ "vitest": "5.1.0",
65
+ "ws": "8.23.0"
59
66
  },
60
67
  "license": "Apache-2.0",
61
68
  "readmeFilename": "README.md",
62
69
  "contributors": [],
63
70
  "peerDependencies": {
64
- "@nodefony/http": "^10.0.0-beta.5",
65
- "nodefony": "^10.0.0-beta.5",
71
+ "@bufbuild/protobuf": "^2.7.0",
72
+ "@connectrpc/connect": "^2.3.0",
73
+ "@nodefony/http": "^10.0.0-beta.7",
74
+ "nodefony": "^10.0.0-beta.7",
66
75
  "zod": "^4.6.7"
67
76
  },
77
+ "peerDependenciesMeta": {
78
+ "@bufbuild/protobuf": {
79
+ "optional": true
80
+ },
81
+ "@connectrpc/connect": {
82
+ "optional": true
83
+ }
84
+ },
68
85
  "files": [
69
86
  "dist",
70
87
  "docs"
@@ -72,7 +89,7 @@
72
89
  "publishConfig": {
73
90
  "access": "public"
74
91
  },
75
- "homepage": "https://nodefony.github.io/nodefony-core/",
92
+ "homepage": "https://nodefony.net/",
76
93
  "bugs": {
77
94
  "url": "https://github.com/nodefony/nodefony-core/issues"
78
95
  }