@nodefony/framework 10.0.0-alpha.1

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 (96) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +50 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +211 -0
  6. package/dist/nodefony/config/config.js +61 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  8. package/dist/nodefony/controller/AdminApiController.js +163 -0
  9. package/dist/nodefony/controller/ApiKeyController.js +151 -0
  10. package/dist/nodefony/controller/BenchController.js +132 -0
  11. package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
  12. package/dist/nodefony/controller/OAuth2Controller.js +133 -0
  13. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
  14. package/dist/nodefony/controller/SessionAuthController.js +141 -0
  15. package/dist/nodefony/controller/TokenAuthController.js +113 -0
  16. package/dist/nodefony/controller/TotpController.js +129 -0
  17. package/dist/nodefony/controller/WebAuthnController.js +242 -0
  18. package/dist/nodefony/controller/oauthAuthority.js +74 -0
  19. package/dist/nodefony/decorators/routerDecorators.js +967 -0
  20. package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
  21. package/dist/nodefony/interfaces/IController.js +1 -0
  22. package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
  23. package/dist/nodefony/interfaces/IResolver.js +1 -0
  24. package/dist/nodefony/interfaces/IRoute.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/service/AdminBroker.js +106 -0
  27. package/dist/nodefony/service/Eta.js +68 -0
  28. package/dist/nodefony/service/IdempotencyStore.js +136 -0
  29. package/dist/nodefony/service/router.js +243 -0
  30. package/dist/nodefony/src/Controller.js +515 -0
  31. package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
  32. package/dist/nodefony/src/KernelAdminApi.js +1243 -0
  33. package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
  34. package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
  35. package/dist/nodefony/src/Resolver.js +416 -0
  36. package/dist/nodefony/src/ResourceController.js +148 -0
  37. package/dist/nodefony/src/Route.js +476 -0
  38. package/dist/nodefony/src/SyslogAdminApi.js +466 -0
  39. package/dist/nodefony/src/Template.js +15 -0
  40. package/dist/nodefony/src/configMutation.js +186 -0
  41. package/dist/nodefony/src/docsReader.js +929 -0
  42. package/dist/nodefony/src/idempotency.js +137 -0
  43. package/dist/nodefony/src/idempotencyGc.js +32 -0
  44. package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
  45. package/dist/nodefony/src/scopeCatalog.js +40 -0
  46. package/dist/nodefony/src/syslogFilters.js +51 -0
  47. package/dist/types/index.d.ts +96 -0
  48. package/dist/types/nodefony/config/config.d.ts +41 -0
  49. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  50. package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
  51. package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
  52. package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
  53. package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
  54. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
  55. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
  56. package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
  57. package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
  58. package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
  59. package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
  60. package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
  61. package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
  62. package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
  63. package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
  64. package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
  65. package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
  66. package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
  67. package/dist/types/nodefony/interfaces/index.d.ts +5 -0
  68. package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
  69. package/dist/types/nodefony/service/Eta.d.ts +25 -0
  70. package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
  71. package/dist/types/nodefony/service/router.d.ts +53 -0
  72. package/dist/types/nodefony/src/Controller.d.ts +193 -0
  73. package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
  74. package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
  75. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
  76. package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
  77. package/dist/types/nodefony/src/Resolver.d.ts +165 -0
  78. package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
  79. package/dist/types/nodefony/src/Route.d.ts +192 -0
  80. package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
  81. package/dist/types/nodefony/src/Template.d.ts +8 -0
  82. package/dist/types/nodefony/src/configMutation.d.ts +109 -0
  83. package/dist/types/nodefony/src/docsReader.d.ts +369 -0
  84. package/dist/types/nodefony/src/idempotency.d.ts +96 -0
  85. package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
  86. package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
  87. package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
  88. package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
  89. package/docs/admin.md +451 -0
  90. package/docs/controller.md +645 -0
  91. package/docs/decorateurs.md +845 -0
  92. package/docs/idempotence.md +741 -0
  93. package/docs/index.md +151 -0
  94. package/docs/routing.md +648 -0
  95. package/docs/templates.md +380 -0
  96. package/package.json +83 -0
@@ -0,0 +1,86 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Vue MINIMALE du service d'émission de jetons, côté **publication**
6
+ * (`tokenService`, posé au container par `@nodefony/security`). Contrat
7
+ * structurel local : framework ne dépend JAMAIS de security — couplage par nom
8
+ * de service, comme `authFlow`/`adminBroker`.
9
+ *
10
+ * Les deux méthodes disent l'essentiel du partage des rôles : security DÉCIDE
11
+ * (peut-on se déclarer émetteur ?) et FOURNIT la matière (les clés publiques) ;
12
+ * framework se contente d'ouvrir la porte.
13
+ */
14
+ export interface IIssuerPublisher {
15
+ /** Émetteur canonique publiable, ou `null` si rien ne doit l'être. */
16
+ publishedIssuer(): string | null;
17
+ /** Jeu de clés PUBLIQUES de signature (jamais de clé privée). */
18
+ getPublicJWKS(): Promise<unknown>;
19
+ }
20
+ /**
21
+ * Les deux documents qui rendent une application Nodefony **découvrable** comme
22
+ * émetteur de jetons — RFC 8414 :
23
+ *
24
+ * - `GET /.well-known/oauth-authorization-server` — métadonnées d'émetteur
25
+ * (`issuer`, `jwks_uri`) ; c'est le seul document qu'un tiers sait trouver,
26
+ * puisqu'il ne le lit pas mais le CONSTRUIT depuis l'identifiant d'émetteur
27
+ * par insertion de chemin (§3.1).
28
+ * - `GET /.well-known/jwks.json` — le jeu de clés publiques lui-même.
29
+ *
30
+ * ## Pourquoi ces routes existent
31
+ *
32
+ * Sans elles, `getPublicJWKS()` n'a qu'un usage interne : Nodefony vérifie ses
33
+ * propres jetons en mémoire. Un tiers — une autre application Nodefony, un
34
+ * agent, un service — ne peut donc PAS valider une signature émise ici, même en
35
+ * connaissant l'URL des clés : il n'y en a pas. C'est le symétrique exact du
36
+ * rôle serveur de ressource : là, on refusait en disant où aller ; ici, on
37
+ * permet à quelqu'un d'autre de vérifier ce qu'on a signé.
38
+ *
39
+ * ## Ce qui les monte, et ce qui les retient
40
+ *
41
+ * Montées par le module framework UNIQUEMENT si `tokenService.publishedIssuer()`
42
+ * rend une valeur — c'est-à-dire si le JWT est actif, `security.jwt.jwks` vrai,
43
+ * et l'émetteur écrit sous forme d'URL https. Sinon les routes **n'existent
44
+ * pas** (`404`, zéro surface) : un document creux apprendrait à un client qu'il
45
+ * y a quelque chose à découvrir sans lui donner de quoi le faire.
46
+ *
47
+ * Le montage ne suffit pas : servir encore exige que la requête entre par
48
+ * **l'autorité de l'émetteur** ({@link IssuerMetadataController.metadata}). Un
49
+ * serveur écoute plusieurs adresses ; l'émetteur n'en désigne qu'une.
50
+ *
51
+ * `bypassFirewall: true` : ces documents sont publics par construction. Un JWKS
52
+ * derrière une authentification serait un non-sens — il sert précisément à
53
+ * vérifier les jetons de ceux qui ne sont pas encore authentifiés. Ils ne
54
+ * révèlent rien de secret : des clés PUBLIQUES et un identifiant, jamais `d`.
55
+ *
56
+ * @remarks Le chemin des métadonnées est **dérivé** de l'émetteur
57
+ * (`authorizationServerMetadataPath`) et non écrit en dur : un émetteur porteur
58
+ * d'un chemin se publie SOUS ce chemin (RFC 8414 §3.1), et c'est la même
59
+ * fonction qui sert au lecteur, dans `@nodefony/security`.
60
+ */
61
+ declare class IssuerMetadataController extends Controller {
62
+ #private;
63
+ constructor(context: ContextType);
64
+ /**
65
+ * `GET /.well-known/oauth-authorization-server` — métadonnées d'émetteur.
66
+ *
67
+ * @returns le document RFC 8414, ou `404` si la publication a été coupée
68
+ * après le montage des routes
69
+ */
70
+ metadata(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
71
+ /**
72
+ * `GET /.well-known/jwks.json` — clés publiques de signature.
73
+ *
74
+ * @returns le JWK Set (paramètres publics seuls)
75
+ */
76
+ jwks(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
77
+ }
78
+ /**
79
+ * Monte les deux documents d'émetteur — appelé par le module framework à
80
+ * `onKernelReady`, seulement si `tokenService.publishedIssuer()` répond.
81
+ *
82
+ * @param frameworkModule - module porteur des routes
83
+ * @param issuer - émetteur canonique, qui DÉTERMINE le chemin des métadonnées
84
+ */
85
+ export declare function mountIssuerMetadataRoutes(frameworkModule: Module, issuer: string): void;
86
+ export default IssuerMetadataController;
@@ -0,0 +1,81 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Vue MINIMALE du service `oauth2` (`@nodefony/security`) — couplage par NOM
6
+ * (framework ne dépend ni de security ni d'arctic). Contrat structurel imposé par
7
+ * cast (`this.get<…>`), aucune liaison de build.
8
+ */
9
+ export interface IOAuth2Service {
10
+ isEnabled(): boolean;
11
+ listProviders(): string[];
12
+ getRedirects(provider?: string): {
13
+ success: string;
14
+ failure: string;
15
+ };
16
+ createAuthorization(provider: string): Promise<{
17
+ url: string;
18
+ state: string;
19
+ codeVerifier: string | null;
20
+ }>;
21
+ exchangeAndProvision(provider: string, code: string, codeVerifier: string | null, returnedIss: string | null): Promise<{
22
+ identifier: string;
23
+ }>;
24
+ }
25
+ /** Vue minimale d'une session — porte l'état du flux OAuth (anti-CSRF/anti-replay). */
26
+ export interface IOAuth2Session {
27
+ get(key: string): unknown;
28
+ set(key: string, value: unknown): unknown;
29
+ save(): Promise<unknown>;
30
+ }
31
+ /** Vue minimale du flux de session BFF (`authFlow`) consommée ici. */
32
+ export interface IOAuth2BffFlow {
33
+ /**
34
+ * @param reason - facteur d'authentification journalisé par l'audit
35
+ * (`"oauth"` ici). Omis, il retombe sur `"federated"`, qui ne distingue plus
36
+ * un login social d'une passkey dans le journal.
37
+ */
38
+ establishSessionFor(context: ContextType, identifier: string, reason?: string): Promise<unknown>;
39
+ /** Garantit une session (anonyme) pour porter `state`/`code_verifier`. */
40
+ ensureSession(context: ContextType): Promise<IOAuth2Session | null>;
41
+ }
42
+ /**
43
+ * Endpoints HTTP du **social login OAuth 2.0** (P6 J9) — adaptateurs MINCES
44
+ * au-dessus du service `oauth2` (`@nodefony/security`) :
45
+ *
46
+ * - `GET /nodefony/security/api/oauth2/{provider}/authorize` — démarre le flux :
47
+ * pose `state`+`code_verifier` en session (anonyme), redirige (302) vers le
48
+ * fournisseur.
49
+ * - `GET /nodefony/security/api/oauth2/{provider}/callback` — valide le `state`
50
+ * (anti-CSRF), échange le `code`, provisionne le Shadow User et OUVRE la
51
+ * session BFF (302 vers `successRedirect`).
52
+ *
53
+ * Montés UNIQUEMENT si le service `oauth2` existe (social login activé) — 404
54
+ * sinon, zéro surface.
55
+ *
56
+ * @remarks `bypassFirewall` : ces routes SONT (ou précèdent) le mécanisme d'auth
57
+ * (l'utilisateur est anonyme pendant tout l'aller-retour). Le firewall, sur l'aire
58
+ * data plane, déclencherait un deadlock identique au login BFF / WebAuthn login.
59
+ * La session anonyme ne porte que `state`/`verifier` ; `establishSessionFor`
60
+ * régénère l'ID (anti-fixation) à la promotion.
61
+ */
62
+ declare class OAuth2Controller extends Controller {
63
+ #private;
64
+ constructor(context: ContextType);
65
+ /**
66
+ * Liste PUBLIQUE des fournisseurs activés (configurés ET connus du registre).
67
+ * Consommé par l'UI de login pour n'afficher QUE les boutons opérationnels —
68
+ * jamais de bouton mort. Aucun secret n'est exposé (uniquement les noms).
69
+ */
70
+ providers(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
71
+ /** Démarre le flux : URL d'autorisation + état anti-replay en session, 302. */
72
+ authorize(provider: string): Promise<void | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
73
+ /** Valide `state`, échange le `code`, ouvre la session BFF (302). */
74
+ callback(provider: string): Promise<void | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
75
+ }
76
+ /**
77
+ * Monte les routes du social login OAuth — appelé par le module framework à
78
+ * `onKernelReady`, seulement si le service `oauth2` est présent.
79
+ */
80
+ export declare function mountOAuth2Routes(frameworkModule: Module): void;
81
+ export default OAuth2Controller;
@@ -0,0 +1,147 @@
1
+ import type { Module } from "nodefony";
2
+ import type { IProtectedResourceInput } from "nodefony";
3
+ import type { ContextType } from "@nodefony/http";
4
+ import Controller from "../src/Controller.js";
5
+ /**
6
+ * Vue MINIMALE du service qui sait ce que l'application PROTÈGE, côté
7
+ * **publication**. Contrat structurel local : framework ne dépend JAMAIS de
8
+ * `@nodefony/security` — couplage par nom de service (`firewall`), comme
9
+ * `tokenService`/`authFlow`/`adminBroker`.
10
+ *
11
+ * Le partage des rôles est le même que pour l'émetteur : security DÉCIDE (quelles
12
+ * zones déclarent une ressource, quels serveurs d'autorisation les servent) et
13
+ * framework se contente d'ouvrir la porte.
14
+ */
15
+ export interface IProtectedResourcePublisher {
16
+ /**
17
+ * Ressources protégées à publier — vide si l'application n'en protège aucune.
18
+ *
19
+ * Chaque entrée décrit une ressource telle qu'un client l'atteint, et les
20
+ * serveurs d'autorisation capables d'émettre un jeton pour elle.
21
+ */
22
+ publishedProtectedResources(): readonly IProtectedResourceInput[];
23
+ }
24
+ /**
25
+ * Le document qui rend un refus APPRENABLE — RFC 9728.
26
+ *
27
+ * ## Le trou que ce controller ferme
28
+ *
29
+ * Un `401` d'une zone qui déclare sa ressource porte désormais un défi complet :
30
+ *
31
+ * ```
32
+ * WWW-Authenticate: Bearer resource_metadata="https://app/.well-known/oauth-protected-resource/api"
33
+ * ```
34
+ *
35
+ * Sans ce controller, cette URL rendait **404** : un pointeur syntaxiquement
36
+ * conforme qui ne mène nulle part. Le client le lit, le suit, trouve une erreur,
37
+ * et conclut qu'il n'y a pas d'autorisation ici — c'est-à-dire exactement
38
+ * l'inverse de ce que le refus voulait lui apprendre. Seul `@nodefony/devkit`
39
+ * publiait un document, et uniquement pour SA porte MCP.
40
+ *
41
+ * ## Une route par CHEMIN, un document par AUTORITÉ
42
+ *
43
+ * La RFC 9728 §3.1 **insère** le chemin de la ressource dans l'URL bien connue
44
+ * (« Using path components enables supporting multiple resources per host ») :
45
+ * deux ressources de chemins distincts ont donc deux URL distinctes, et l'on
46
+ * monte une route par chemin. Deux ressources qui partagent le chemin mais pas
47
+ * l'autorité (`https://a.example/api` et `https://b.example/api`) partagent en
48
+ * revanche la même route : c'est alors l'autorité demandée qui départage, à la
49
+ * requête.
50
+ *
51
+ * ## Ce qui retient la publication
52
+ *
53
+ * Rien n'est monté si le service `firewall` est absent, ou s'il ne déclare
54
+ * aucune ressource : **`404`, zéro surface**. Un document creux apprendrait à un
55
+ * client qu'il y a quelque chose à découvrir sans lui donner de quoi le faire —
56
+ * et la spécification MCP l'interdit explicitement (« MUST include […] at least
57
+ * one authorization server »).
58
+ *
59
+ * `bypassFirewall: true` : ce document est public par construction. Le placer
60
+ * derrière l'authentification serait circulaire — il sert précisément à
61
+ * expliquer comment s'authentifier à qui ne l'est pas encore. Il ne révèle rien
62
+ * de secret : une URI publique, des émetteurs, des scopes.
63
+ *
64
+ * @see references/rfc/ietf/rfc9728.txt — métadonnées de la ressource protégée
65
+ * @see references/rfc/ietf/rfc8707.txt — l'audience, qui LIE un jeton à CE service
66
+ */
67
+ declare class ProtectedResourceMetadataController extends Controller {
68
+ #private;
69
+ constructor(context: ContextType);
70
+ /**
71
+ * `GET /.well-known/oauth-protected-resource/<chemin>` — RFC 9728 §3.
72
+ *
73
+ * 🔴 Le document n'est servi que sur l'autorité de SA ressource. Un client
74
+ * conforme rejette un document dont la `resource` ne correspond pas à l'URI
75
+ * qu'il interrogeait (§3.3) — et un client réel s'arrête là au lieu de
76
+ * continuer sans authentification. C'est la faille déjà vécue sur le document
77
+ * d'émetteur, transposée : la règle est partagée (`onDeclaredAuthority`).
78
+ *
79
+ * @returns le document JSON, ou `404` si aucune ressource déclarée ne
80
+ * correspond au chemin ET à l'autorité demandés
81
+ */
82
+ metadata(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
83
+ }
84
+ /**
85
+ * Vue minimale du conteneur de services — assez pour balayer, pas plus.
86
+ *
87
+ * Le type complet vit dans `nodefony` mais l'importer ici lierait ce fichier au
88
+ * cœur pour une seule opération de lecture.
89
+ */
90
+ export interface IServiceScan {
91
+ keys(): string[];
92
+ get<T = unknown>(name: string): T | null;
93
+ }
94
+ /**
95
+ * Rassemble ce que TOUS les services du conteneur déclarent protéger.
96
+ *
97
+ * ⭐ **Pourquoi balayer plutôt que demander à un service nommé.** Les ressources
98
+ * protégées d'une application n'ont pas une source unique : le pare-feu en
99
+ * déclare (une zone qui exige un jeton tiers), et un module peut en déclarer une
100
+ * de son propre chef — la porte MCP de `@nodefony/devkit` en est le premier cas.
101
+ * Interroger le seul `firewall` aurait laissé chaque autre module monter SON
102
+ * document, c'est-à-dire recopier cette règle autant de fois qu'il y a de
103
+ * sources, avec la collision de chemin en prime : `Router.createRoute` empile
104
+ * sans vérifier, et la seconde route ne serait jamais atteinte.
105
+ *
106
+ * Un **registre** aurait fait le même travail, au prix d'une dépendance d'ordre :
107
+ * un module qui s'enregistre après le `onKernelReady` du framework ne serait
108
+ * jamais publié, et rien ne le dirait. Le balayage n'a pas ce défaut — les
109
+ * services sont tous en place bien avant, et la question se pose au moment où
110
+ * l'on monte.
111
+ *
112
+ * Le contrat est **structurel** : porter la méthode suffit, aucun import, aucune
113
+ * interface à implémenter formellement — le même couplage que `tokenService` ou
114
+ * `adminBroker`.
115
+ *
116
+ * @param container - conteneur de services de l'application
117
+ * @returns les ressources déclarées, dans l'ordre des services
118
+ */
119
+ export declare function collectProtectedResources(container: IServiceScan): readonly IProtectedResourceInput[];
120
+ /**
121
+ * Chemins bien connus à monter pour un jeu de ressources déclarées.
122
+ *
123
+ * Fonction **pure**, exportée pour être éprouvée sans serveur : c'est elle qui
124
+ * porte les deux décisions qui font la différence entre un document servi et un
125
+ * `404` — la dérivation du chemin (insertion RFC 9728 §3.1, jamais une
126
+ * concaténation) et la déduplication.
127
+ *
128
+ * Deux zones peuvent parfaitement déclarer la même ressource (typiquement une
129
+ * zone HTTP et son pendant WebSocket) ; deux hôtes virtuels peuvent en déclarer
130
+ * deux différentes sous le même chemin. Dans les deux cas il ne faut monter
131
+ * qu'**une** route : `Router.createRoute` empile sans vérifier, et la seconde
132
+ * ne serait jamais atteinte — une collision parfaitement silencieuse.
133
+ *
134
+ * @param resources - ressources déclarées par le publieur
135
+ * @returns les chemins distincts à servir, dans l'ordre de déclaration
136
+ */
137
+ export declare function protectedResourceRoutePaths(resources: readonly IProtectedResourceInput[]): string[];
138
+ /**
139
+ * Monte les documents de ressource protégée — appelé par le module framework à
140
+ * `onKernelReady`, seulement si un publieur déclare au moins une ressource.
141
+ *
142
+ * @param frameworkModule - module porteur des routes
143
+ * @param resources - ressources déclarées, qui DÉTERMINENT les chemins montés
144
+ * @returns le nombre de routes montées
145
+ */
146
+ export declare function mountProtectedResourceRoutes(frameworkModule: Module, resources: readonly IProtectedResourceInput[]): number;
147
+ export default ProtectedResourceMetadataController;
@@ -0,0 +1,74 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Vue MINIMALE du flux de session BFF que consomme ce controller — le service
6
+ * `authFlow` est posé au container par `@nodefony/security` (P6 J3). Contrat
7
+ * structurel local : framework ne dépend JAMAIS de security (c'est http, sous
8
+ * framework, que security décore) ; le couplage se fait par nom de service,
9
+ * comme `adminBroker`.
10
+ */
11
+ /** Issue d'un login BFF — identité établie, ou second facteur (2FA) requis. */
12
+ type ILoginOutcome = {
13
+ status: "authenticated";
14
+ user: unknown;
15
+ } | {
16
+ status: "mfa_required";
17
+ methods: string[];
18
+ };
19
+ export interface ISessionAuthFlow {
20
+ login(context: ContextType, identifier: unknown, password: unknown): Promise<ILoginOutcome>;
21
+ completeMfaLogin(context: ContextType, code: unknown): Promise<unknown>;
22
+ logout(context: ContextType): Promise<boolean>;
23
+ me(context: ContextType): Promise<unknown | null>;
24
+ }
25
+ /**
26
+ * Endpoints HTTP du flux de session BFF — adaptateurs MINCES au-dessus du
27
+ * service `authFlow` (`@nodefony/security`) :
28
+ *
29
+ * - `POST /nodefony/security/api/auth/login` — body JSON `{username, password}`
30
+ * - `POST /nodefony/security/api/auth/logout` — idempotent
31
+ * - `GET /nodefony/security/api/auth/me` — identité de la session courante
32
+ *
33
+ * Montés par le module framework UNIQUEMENT si le service `authFlow` existe
34
+ * (module security chargé) — sans lui, les routes n'existent pas (404, zéro
35
+ * surface). Remplace les mocks `/nodefony/studio/api/auth/*` de Studio.
36
+ *
37
+ * Erreurs : mappées par DUCK-TYPING sur `code` (401/429) — framework ne peut
38
+ * pas importer les classes d'erreur de security. Un 429 reporte le
39
+ * `Retry-After` (RFC 6585) posé par le throttler NIST ; tout le reste remonte
40
+ * au pipeline d'erreurs standard (500, détail loggé jamais fuité).
41
+ *
42
+ * @remarks Dérogation RFC 7235 §3.1 DÉLIBÉRÉE : les 401 de `login`/`me` ne
43
+ * portent PAS de `WWW-Authenticate` — aucun scheme HTTP ne décrit un cookie de
44
+ * session, et un challenge `Basic` mensonger déclencherait le popup natif du
45
+ * navigateur (l'anti-pattern que le BFF élimine). Les zones du firewall, elles,
46
+ * restent strictement conformes (challenge du premier authenticator qui en
47
+ * déclare un).
48
+ */
49
+ declare class SessionAuthController extends Controller {
50
+ #private;
51
+ constructor(context: ContextType);
52
+ /** Login BFF : credential JSON présenté UNE fois → cookie de session opaque. */
53
+ login(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
54
+ /**
55
+ * Second facteur (TOTP ou code de récupération) après un `login` ayant répondu
56
+ * `202 mfaRequired`. Body JSON `{ code }`. Succès → 200 + identité ; code
57
+ * absent/invalide → 401 (uniforme) ; trop de tentatives → 429 (Retry-After).
58
+ */
59
+ loginTotp(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
60
+ /** Détruit la session (storage + cookie). Toujours 200 (idempotent). */
61
+ logout(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
62
+ /** Identité de la session courante, re-résolue (rôles frais), ou 401. */
63
+ me(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
64
+ }
65
+ /**
66
+ * Monte les routes du flux de session BFF — appelé par le module framework à
67
+ * `onKernelReady`, seulement si le service `authFlow` est présent.
68
+ *
69
+ * Routes nommées `security.auth.*` (espace data plane
70
+ * `/nodefony/security/api/*`, convention « toujours ≥ 3 segments »). HTTP-only :
71
+ * la sémantique session-par-socket du pont WS-RPC n'est pas conçue (P6+).
72
+ */
73
+ export declare function mountSessionAuthRoutes(frameworkModule: Module): void;
74
+ export default SessionAuthController;
@@ -0,0 +1,56 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Vue MINIMALE du service d'émission de jetons (`tokenService`, posé au container
6
+ * par `@nodefony/security` P6 J4). Contrat structurel local : framework ne dépend
7
+ * JAMAIS de security — couplage par nom de service (comme `authFlow`/`adminBroker`).
8
+ */
9
+ export interface ITokenIssuer {
10
+ /** `true` si l'émission JWT est opérationnelle (JWT activé + store prêt). */
11
+ isEnabled(): boolean;
12
+ /** Émet access+refresh après vérification d'un credential (grant M2M/CLI). */
13
+ issueForCredentials(identifier: unknown, password: unknown, scopes?: string[], resource?: unknown): Promise<unknown>;
14
+ /** Rotation d'un refresh token (nouveau couple, ancien révoqué). */
15
+ refresh(rawRefresh: unknown, resource?: unknown): Promise<unknown>;
16
+ }
17
+ /**
18
+ * Endpoints HTTP d'émission/rotation de **JWT** — adaptateurs MINCES au-dessus du
19
+ * service `tokenService` (`@nodefony/security`) :
20
+ *
21
+ * - `POST /nodefony/security/api/token` — body `{username, password, scope?}`
22
+ * → `{access_token, refresh_token, token_type, expires_in, scope}` (RFC 6749 §5.1)
23
+ * - `POST /nodefony/security/api/token/refresh` — body `{refresh_token}` → rotation
24
+ *
25
+ * Montés par le module framework UNIQUEMENT si le service `tokenService` existe
26
+ * (module security chargé + JWT activé) — sans lui, les routes n'existent pas
27
+ * (404, zéro surface).
28
+ *
29
+ * Le JWT part en **réponse JSON** (Bearer), JAMAIS en cookie ni en URL (anti
30
+ * fuite / non-révocable). Erreurs mappées par DUCK-TYPING sur `code` (401/429) —
31
+ * framework ne peut pas importer les classes d'erreur de security ; un 429
32
+ * reporte le `Retry-After` (RFC 6585) du throttler NIST.
33
+ *
34
+ * @remarks Dérogation RFC 7235 §3.1 (comme `SessionAuthController`) : ces 401 ne
35
+ * portent pas de `WWW-Authenticate` — l'endpoint d'émission n'est pas une
36
+ * ressource protégée par Bearer, il DÉLIVRE le Bearer.
37
+ */
38
+ declare class TokenAuthController extends Controller {
39
+ #private;
40
+ constructor(context: ContextType);
41
+ /** Émission : credential présenté → couple access/refresh. */
42
+ token(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
43
+ /** Rotation : refresh présenté → nouveau couple (ancien révoqué). */
44
+ refresh(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
45
+ }
46
+ /**
47
+ * Monte les routes d'émission/rotation JWT — appelé par le module framework à
48
+ * `onKernelReady`, seulement si le service `tokenService` est présent.
49
+ *
50
+ * Routes nommées `security.token.*` (espace data plane `/nodefony/security/api/*`).
51
+ * `bypassFirewall: true` : ces routes SONT le mécanisme d'émission — l'aire data
52
+ * plane les matcherait sinon (obtenir un token exigerait d'être déjà authentifié,
53
+ * deadlock).
54
+ */
55
+ export declare function mountTokenAuthRoutes(frameworkModule: Module): void;
56
+ export default TokenAuthController;
@@ -0,0 +1,42 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Endpoints HTTP **self-service 2FA TOTP (P6.17)** — console « ma sécurité »,
6
+ * adaptateurs MINCES au-dessus du service `totp` (`@nodefony/security`) :
7
+ *
8
+ * - `POST /nodefony/security/api/totp/enroll` → `{secretBase32, otpauthUri}`
9
+ * (secret affiché 1× pour le QR ; secret PENDING tant que non confirmé)
10
+ * - `POST /nodefony/security/api/totp/confirm` — body `{code}` → `{recoveryCodes}`
11
+ * (active le 2FA ; codes de récupération affichés 1×)
12
+ * - `POST /nodefony/security/api/totp/disable` → `{ok}` (retire le 2FA)
13
+ * - `GET /nodefony/security/api/totp/status` → `{enabled, pending, recoveryCodesRemaining}`
14
+ *
15
+ * **PAS de `bypassFirewall`** (≠ login/totp) : ces routes vivent DANS la zone data
16
+ * plane `^/nodefony/[^/]+/api(/|$)` → **session BFF requise**. Le sujet est
17
+ * TOUJOURS l'utilisateur courant (`authFlow.me`), jamais un paramètre — on n'active
18
+ * /ne désactive jamais le 2FA d'autrui (anti-IDOR). Montés seulement si le service
19
+ * `totp` existe (security chargé + 2FA activé) → 404, zéro surface, sinon.
20
+ */
21
+ declare class TotpController extends Controller {
22
+ #private;
23
+ constructor(context: ContextType);
24
+ /** Démarre l'enrôlement : secret + URI otpauth affichés une seule fois. */
25
+ enroll(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
26
+ /** Confirme l'enrôlement par un 1ᵉʳ code → active + codes de récupération (1×). */
27
+ confirm(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
28
+ /** Désactive le 2FA du porteur courant (retire secret + codes). */
29
+ disable(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
30
+ /** État 2FA du porteur courant (alimente la console « ma sécurité »). */
31
+ status(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
32
+ }
33
+ /**
34
+ * Monte les routes self-service 2FA — appelé par le module framework à
35
+ * `onKernelReady`, seulement si le service `totp` est présent.
36
+ *
37
+ * Routes nommées `security.totp.*` (espace data plane `/nodefony/security/api/*`).
38
+ * **Aucun `bypassFirewall`** : l'aire data plane (session BFF) les garde — gérer
39
+ * son 2FA exige d'être authentifié.
40
+ */
41
+ export declare function mountTotpRoutes(frameworkModule: Module): void;
42
+ export default TotpController;
@@ -0,0 +1,123 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Options de cérémonie renvoyées au navigateur (forme JSON WebAuthn) — seul le
6
+ * `challenge` nous intéresse côté serveur (stocké en session, anti-replay) ; le
7
+ * reste est relayé tel quel au client.
8
+ */
9
+ type CeremonyOptions = {
10
+ challenge: string;
11
+ } & Record<string, unknown>;
12
+ /**
13
+ * Vue MINIMALE du service `webauthn` (`@nodefony/security`) — couplage par NOM
14
+ * (framework ne dépend jamais de security ni de `@simplewebauthn`). Contrat
15
+ * structurel imposé par cast (`this.get<…>`), aucune liaison de build.
16
+ */
17
+ export interface IWebAuthnService {
18
+ isEnabled(): boolean;
19
+ generateRegistrationOptions(user: {
20
+ id: string;
21
+ name: string;
22
+ displayName?: string;
23
+ }): Promise<CeremonyOptions>;
24
+ verifyRegistration(response: unknown, expectedChallenge: string, userId: string, requestOrigin?: string): Promise<{
25
+ id: string;
26
+ }>;
27
+ generateAuthenticationOptions(userId?: string): Promise<CeremonyOptions>;
28
+ verifyAuthentication(response: unknown, expectedChallenge: string, requestOrigin?: string): Promise<{
29
+ userId: string;
30
+ }>;
31
+ listUserCredentials(userId: string): Promise<Array<{
32
+ id: string;
33
+ transports: string[];
34
+ backupState: boolean;
35
+ createdAt: number;
36
+ lastUsedAt: number | null;
37
+ }>>;
38
+ removeUserCredential(userId: string, credentialId: string): Promise<boolean>;
39
+ }
40
+ /** Vue minimale d'une session — porte le challenge de cérémonie (anti-replay). */
41
+ export interface IWebAuthnSession {
42
+ get(key: string): unknown;
43
+ set(key: string, value: unknown): unknown;
44
+ save(): Promise<unknown>;
45
+ }
46
+ /** Vue minimale du flux de session BFF (`authFlow`) consommée ici. */
47
+ export interface IWebAuthnBffFlow {
48
+ me(context: ContextType): Promise<{
49
+ username: string;
50
+ } | null>;
51
+ /**
52
+ * @param reason - facteur d'authentification journalisé par l'audit
53
+ * (`"webauthn"` ici). Omis, il retombe sur `"federated"`, qui ne distingue
54
+ * plus une passkey d'un login social dans le journal.
55
+ */
56
+ establishSessionFor(context: ContextType, identifier: string, reason?: string): Promise<unknown>;
57
+ /** Garantit une session (la démarre si déconnecté) pour porter le challenge. */
58
+ ensureSession(context: ContextType): Promise<IWebAuthnSession | null>;
59
+ }
60
+ /**
61
+ * Endpoints HTTP des cérémonies **WebAuthn / passkeys** (P6 J9) — adaptateurs
62
+ * MINCES au-dessus du service `webauthn` (`@nodefony/security`) :
63
+ *
64
+ * - `POST /nodefony/security/api/webauthn/register/options` — défi de création
65
+ * (utilisateur DÉJÀ connecté : lie un passkey à son compte)
66
+ * - `POST /nodefony/security/api/webauthn/register/verify` — vérifie + stocke
67
+ * - `POST /nodefony/security/api/webauthn/login/options` — défi d'assertion
68
+ * - `POST /nodefony/security/api/webauthn/login/verify` — vérifie + ouvre
69
+ * la session BFF (l'empreinte remplace le mot de passe)
70
+ *
71
+ * Le **challenge** est stocké côté serveur en session (jamais rejouable) entre
72
+ * `options` et `verify`.
73
+ *
74
+ * **Deux conditions distinctes, deux réponses distinctes** : les routes ne sont
75
+ * montées que si le service `webauthn` est dans le container (`framework/index.ts:400`),
76
+ * c'est-à-dire dès que `@nodefony/security` est chargé — sans security, **404**,
77
+ * zéro surface. Le service reste enregistré même passkeys DÉSACTIVÉS
78
+ * (`@services` l'instancie inconditionnellement) : dans ce cas les routes existent
79
+ * et répondent **503** (`isEnabled()` faux). Ne pas lire un 503 comme « route
80
+ * absente », ni un 404 comme « passkeys coupés ».
81
+ *
82
+ * @remarks `bypassFirewall` : `login/*` précède toute authentification ;
83
+ * `register/*` exige une session active, vérifiée ICI (`me()` → 401), pas par le
84
+ * firewall (qui, sur l'aire data plane, déclencherait un deadlock identique au
85
+ * login BFF).
86
+ */
87
+ declare class WebAuthnController extends Controller {
88
+ #private;
89
+ constructor(context: ContextType);
90
+ /** Défi d'enregistrement — l'utilisateur connecté ajoute un passkey. */
91
+ registerOptions(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
92
+ /** Vérifie la réponse d'enregistrement et persiste le credential. */
93
+ registerVerify(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
94
+ /**
95
+ * Défi d'authentification. **Le ciblage ne vient jamais de la requête** : un
96
+ * appelant anonyme obtient un défi découvrable (`allowCredentials` omis), un
97
+ * appelant déjà authentifié obtient le sien (ré-authentification / step-up).
98
+ *
99
+ * @remarks Route en `bypassFirewall` : n'importe qui peut la poster. Peupler
100
+ * `allowCredentials` depuis un identifiant fourni dirait deux choses à cet
101
+ * inconnu — que le compte porte une passkey, et **lesquelles** (W3C WebAuthn
102
+ * L3, « Privacy leak via credential IDs » : un `credentialId` est un
103
+ * identifiant corrélable, exposé il dés-anonymise entre sites et confirme
104
+ * une hypothèse d'identité avec un accès momentané à l'authenticator). La
105
+ * spec propose deux remèdes, ce sont les deux régimes ci-dessous : les
106
+ * credentials découvrables pour l'anonyme, une **authentification préalable**
107
+ * quand on cible vraiment. Verrouillé par
108
+ * `tests/unit/webauthnLoginOptionsPrivacy.test.ts`.
109
+ */
110
+ loginOptions(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
111
+ /** Vérifie l'assertion (empreinte) et OUVRE la session BFF. */
112
+ loginVerify(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
113
+ /** Liste les passkeys de l'utilisateur courant (console « mes appareils »). */
114
+ listCredentials(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
115
+ /** Supprime une passkey DU porteur courant (sinon 404 — anti-IDOR/anti-énumération). */
116
+ removeCredential(id: unknown): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
117
+ }
118
+ /**
119
+ * Monte les routes des cérémonies WebAuthn — appelé par le module framework à
120
+ * `onKernelReady`, seulement si le service `webauthn` est présent.
121
+ */
122
+ export declare function mountWebAuthnRoutes(frameworkModule: Module): void;
123
+ export default WebAuthnController;