@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,79 @@
1
+ import type { IAdminEndpoint, IAdminRegistry } from "nodefony";
2
+ /**
3
+ * Entrée résolue d'un endpoint admin — couple le producteur, sa définition et
4
+ * le chemin absolu monté. Renvoyé par l'introspection du broker (utile à
5
+ * l'auto-doc Studio et aux tests).
6
+ */
7
+ export interface IAdminRoute {
8
+ /** Nom unique de la route framework (clé de dispatch O(1)). */
9
+ name: string;
10
+ /** Namespace du producteur (`IAdminApi.adminNamespace`). */
11
+ namespace: string;
12
+ /** Chemin absolu monté, ex `/nodefony/http/api/sessions`. */
13
+ path: string;
14
+ /** Méthode HTTP effective (défaut résolu). */
15
+ method: string;
16
+ /** Rôle effectif requis (défaut résolu). */
17
+ role: string;
18
+ /** Définition d'origine. */
19
+ endpoint: IAdminEndpoint;
20
+ }
21
+ /**
22
+ * Service du data plane admin — vit dans `@nodefony/framework` car lui seul a
23
+ * le Router pour créer des routes dynamiquement.
24
+ *
25
+ * Rôle : collecter les {@link IAdminApi} produits par les modules (et par le
26
+ * kernel, que framework wrappe), puis monter `/nodefony/<namespace>/api/*` au
27
+ * boot. À l'exécution, pour chaque requête, le broker :
28
+ * 1. adapte le `ContextType` HTTP/WS en `IAdminRequest` (params/query/body/
29
+ * user/roles/requestId) ;
30
+ * 2. applique le RBAC (compare `endpoint.role` à `request.roles`, 403 sinon) ;
31
+ * 3. appelle le `handler`, normalise le retour en `IAdminResponse` ;
32
+ * 4. sérialise en JSON et pose `content-type: application/json` + status.
33
+ *
34
+ * @remarks
35
+ * Pattern d'enregistrement = **push** : un module appelle `register()` dans son
36
+ * `onKernelBoot` (cohérent avec `frontendService.registerEntry`). Le kernel ne
37
+ * pouvant pas importer framework, c'est framework qui construit et enregistre
38
+ * l'`IAdminApi` du kernel (lecture de `kernel.modules`, `process`, uptime…).
39
+ *
40
+ * @remarks
41
+ * Convention de routage figée (cf CLAUDE.md Studio) : data plane toujours en
42
+ * **≥ 3 segments** `/nodefony/<module>/api/*` — jamais une route admin
43
+ * mono-segment `/nodefony/<module>` (collision avec le fallback SPA Studio).
44
+ *
45
+ * Étend {@link IAdminRegistry} (core) : un producteur s'enregistre via la vue
46
+ * minimale `IAdminRegistry` sans dépendre de framework. Ce contrat ajoute le
47
+ * montage des routes et la résolution — réservés à framework (seul à avoir le
48
+ * Router). `getApi`/`register`/etc. portent un nom non-`get` pour ne pas masquer
49
+ * `Service.get` dans l'implémentation (`AdminBroker extends Service`).
50
+ */
51
+ export interface IAdminBroker extends IAdminRegistry {
52
+ /** Préfixe racine réservé au framework. Toujours `"/nodefony"`. */
53
+ readonly rootPrefix: string;
54
+ /** Segment marqueur du data plane. Toujours `"api"`. */
55
+ readonly apiSegment: string;
56
+ /** Rôle exigé par défaut quand un endpoint n'en précise pas. */
57
+ readonly defaultRole: string;
58
+ /**
59
+ * Monte toutes les routes de tous les producteurs enregistrés via le Router
60
+ * (`Router.createRoute`). Appelé une fois après le boot des modules, quand
61
+ * tous les `register()` ont eu lieu. Idempotent (no-op si déjà monté).
62
+ */
63
+ mountAll(): void;
64
+ /**
65
+ * Calcule le chemin absolu d'un endpoint sans le monter.
66
+ * `("http", "sessions")` → `"/nodefony/http/api/sessions"`.
67
+ */
68
+ resolvePath(namespace: string, endpointPath: string): string;
69
+ /**
70
+ * Résout une route admin par son nom framework — lookup O(1) utilisé par le
71
+ * controller pont (`AdminApiController.dispatch`) à chaque requête.
72
+ */
73
+ resolve(routeName: string): IAdminRoute | undefined;
74
+ /**
75
+ * Introspection : toutes les routes admin résolues (montées ou montables).
76
+ * Source de l'auto-doc Studio et des tests de non-collision.
77
+ */
78
+ routes(): readonly IAdminRoute[];
79
+ }
@@ -0,0 +1,38 @@
1
+ import type { contextRequest, HTTPMethod, HttpResponse, Http2Response, WebsocketResponse, Session, ContextType } from "@nodefony/http";
2
+ import type { Module, FileClass } from "nodefony";
3
+ import type { OutgoingHttpHeaders } from "node:http";
4
+ import type { ReadStream } from "node:fs";
5
+ import type { IRoute } from "./IRoute.js";
6
+ export interface IController {
7
+ readonly route?: IRoute | null;
8
+ request: contextRequest;
9
+ response: HttpResponse | Http2Response | WebsocketResponse | null;
10
+ context?: ContextType;
11
+ session?: Session | null;
12
+ method?: HTTPMethod;
13
+ queryGet: Record<string, unknown>;
14
+ query: Record<string, unknown>;
15
+ queryFile: unknown[];
16
+ queryPost: Record<string, unknown>;
17
+ module?: Module;
18
+ setContext(context: ContextType): void;
19
+ setContextJson(encoding?: BufferEncoding): unknown;
20
+ setContextHtml(encoding?: BufferEncoding): unknown;
21
+ render(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: Record<string, string | number>): Promise<unknown>;
22
+ renderResponse(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: OutgoingHttpHeaders): Promise<HttpResponse | Http2Response | WebsocketResponse>;
23
+ renderView(path: string, param?: Record<string, unknown>, status?: string | number, headers?: Record<string, string | number>): Promise<HttpResponse | Http2Response | WebsocketResponse>;
24
+ renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): Promise<unknown>;
25
+ setRoute(route: IRoute): IRoute;
26
+ getSession(): Session | undefined | null;
27
+ redirect(url: string, status?: string | number, headers?: Record<string, string | number>): void;
28
+ getFlashBag(key: string): unknown;
29
+ setFlashBag(key: string, value: unknown): unknown;
30
+ addFlash(key: string, value: unknown): unknown;
31
+ forward(name: string, param?: unknown): unknown;
32
+ /** @deprecated Bloque l'event-loop (`lstatSync`). Préférer `getFileAsync`. */
33
+ getFile(file: FileClass | string): FileClass;
34
+ /** Variante async de `getFile` (stat non bloquant via `FileClass.from`). */
35
+ getFileAsync(file: FileClass | string): Promise<FileClass>;
36
+ renderFileDownload(file: unknown, options?: unknown, headers?: OutgoingHttpHeaders): Promise<ReadStream>;
37
+ streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: Record<string, unknown>): Promise<ReadStream>;
38
+ }
@@ -0,0 +1 @@
1
+ export type { IIdempotencyStore, IIdempotencyKeyEntry, IIdempotencyListQuery, IdempotencyOutcome, IdempotentResponse, } from "nodefony";
@@ -0,0 +1,25 @@
1
+ import type { ContextType, HttpError } from "@nodefony/http";
2
+ import type { Injector } from "nodefony";
3
+ import type { ControllerConstructor } from "../src/Route.js";
4
+ import type { IRoute } from "./IRoute.js";
5
+ import type { IController } from "./IController.js";
6
+ export interface IResolver {
7
+ injector?: Injector | null;
8
+ controller: ControllerConstructor | null;
9
+ actionName?: string;
10
+ action?: (...args: unknown[]) => unknown;
11
+ context: ContextType;
12
+ readonly route: IRoute | null;
13
+ resolve: boolean;
14
+ variables: unknown[];
15
+ exception?: HttpError | Error | null;
16
+ acceptedProtocol: string | null;
17
+ bypassFirewall: boolean;
18
+ match(route: IRoute, context: ContextType, cleanPath?: string): unknown;
19
+ getMatchedParams(): Record<string, unknown>;
20
+ parsePathernController(name: string): void;
21
+ getAction(name: string): ((...args: unknown[]) => unknown) | null;
22
+ newController(context?: ContextType): Promise<IController | object>;
23
+ callController(data?: unknown[], reload?: boolean): Promise<unknown>;
24
+ returnController(result: unknown): Promise<unknown>;
25
+ }
@@ -0,0 +1,31 @@
1
+ import type { HTTPMethod, SchemeType, ContextType } from "@nodefony/http";
2
+ import type { ControllerConstructor, RouteRequirements } from "../src/Route.js";
3
+ export interface IRoute {
4
+ name: string;
5
+ path?: string;
6
+ controller?: ControllerConstructor;
7
+ classMethod?: string;
8
+ prefix?: string;
9
+ method?: HTTPMethod;
10
+ schemes?: SchemeType;
11
+ pattern?: RegExp;
12
+ variables: unknown[];
13
+ defaults: Partial<Record<string, unknown>>;
14
+ requirements: Partial<RouteRequirements>;
15
+ hash?: string;
16
+ host?: string | string[];
17
+ hostRegexp?: RegExp[];
18
+ bypassFirewall: boolean;
19
+ filePath?: string;
20
+ match(context: ContextType, cleanPath?: string): unknown[] | null | undefined;
21
+ compile(): RegExp;
22
+ toString(): string;
23
+ toObject(): object;
24
+ setPrefix(prefix?: string): void;
25
+ setPattern(pattern?: string): string;
26
+ generateId(): string;
27
+ addRequirement<K extends keyof RouteRequirements>(key: K, value: RouteRequirements[K]): RouteRequirements[K] | undefined;
28
+ getRequirement<K extends keyof RouteRequirements>(key: K): RouteRequirements[K] | undefined;
29
+ hasRequirements(): number;
30
+ matchRequirements(context: ContextType): boolean;
31
+ }
@@ -0,0 +1,5 @@
1
+ export type { IController } from "./IController.js";
2
+ export type { IRoute } from "./IRoute.js";
3
+ export type { IResolver } from "./IResolver.js";
4
+ export type { IAdminBroker, IAdminRoute } from "./IAdminBroker.js";
5
+ export type { IIdempotencyStore, IdempotencyOutcome, IdempotentResponse, } from "./IIdempotencyStore.js";
@@ -0,0 +1,37 @@
1
+ import { Service, Module } from "nodefony";
2
+ import type { IAdminApi } from "nodefony";
3
+ import type { IAdminBroker, IAdminRoute } from "../interfaces/IAdminBroker.js";
4
+ /**
5
+ * Implémentation du data plane admin (Studio) — collecte les {@link IAdminApi}
6
+ * et monte `/nodefony/<namespace>/api/*` via le Router.
7
+ *
8
+ * Vit dans `@nodefony/framework` : seul niveau qui possède le Router. Le
9
+ * contrat producteur (`IAdminApi`) vit dans le core (inversion de dépendance).
10
+ *
11
+ * @see IAdminBroker pour le contrat public + la convention de routage.
12
+ */
13
+ declare class AdminBroker extends Service implements IAdminBroker {
14
+ readonly rootPrefix = "/nodefony";
15
+ readonly apiSegment = "api";
16
+ readonly defaultRole = "ROLE_NODEFONY_ADMIN";
17
+ /** Producteurs enregistrés, indexés par namespace. */
18
+ private producers;
19
+ /** Routes résolues, indexées par nom de route (dispatch O(1)). */
20
+ private byRouteName;
21
+ /** Module framework propriétaire — requis pour `Router.setController`. */
22
+ private frameworkModule;
23
+ /** Vrai une fois `mountAll()` exécuté (idempotence + verrou de register). */
24
+ private mounted;
25
+ constructor(module: Module);
26
+ register(api: IAdminApi): this;
27
+ unregister(namespace: string): boolean;
28
+ has(namespace: string): boolean;
29
+ getApi(namespace: string): IAdminApi | undefined;
30
+ list(): readonly IAdminApi[];
31
+ resolvePath(namespace: string, endpointPath: string): string;
32
+ resolve(routeName: string): IAdminRoute | undefined;
33
+ routes(): readonly IAdminRoute[];
34
+ mountAll(): void;
35
+ private getRouter;
36
+ }
37
+ export default AdminBroker;
@@ -0,0 +1,25 @@
1
+ import { Module } from "nodefony";
2
+ import { Eta as EtaEngine } from "eta";
3
+ import Template from "../src/Template.js";
4
+ declare class Eta extends Template {
5
+ engine: EtaEngine;
6
+ constructor(module: Module);
7
+ /**
8
+ * Rend un template depuis une chaîne source (chemin chaud du Controller).
9
+ *
10
+ * @param str - source du template Eta
11
+ * @param data - locals injectés dans le template
12
+ * @returns le rendu HTML/texte
13
+ */
14
+ render(str: string, data?: Record<string, unknown>): Promise<string>;
15
+ /**
16
+ * Rend un template depuis un chemin de fichier (lecture async non bloquante).
17
+ *
18
+ * @param path - chemin absolu du fichier `.eta`
19
+ * @param data - locals injectés dans le template
20
+ * @returns le rendu HTML/texte
21
+ * @throws Si la lecture ou le parsing échoue (loggé en ERROR).
22
+ */
23
+ renderFile(path: string, data?: Record<string, unknown>): Promise<string>;
24
+ }
25
+ export default Eta;
@@ -0,0 +1,45 @@
1
+ import { Service, Module } from "nodefony";
2
+ import type { IPage } from "nodefony";
3
+ import type { IIdempotencyKeyEntry, IIdempotencyListQuery, IIdempotencyStore, IdempotencyOutcome, IdempotentResponse } from "../interfaces/IIdempotencyStore.js";
4
+ /**
5
+ * Implémentation **mémoire** (per-pod) de {@link IIdempotencyStore} — cache borné
6
+ * de dédup des mutations admin rejouées.
7
+ *
8
+ * Vit dans `@nodefony/framework` (niveau qui possède le data plane admin) et
9
+ * s'enregistre comme service DI `idempotencyStore` (manifeste `@services`).
10
+ *
11
+ * **Perf/mémoire** : `Map` allouée **lazy** au 1ᵉʳ `begin` (le store ne sert que
12
+ * les mutations admin = cold path) ; aucun timer/listener (purge passive +
13
+ * éviction FIFO au cap, payées seulement quand on écrit). Aucun coût tant
14
+ * qu'aucune mutation idempotente n'est invoquée.
15
+ *
16
+ * @see IIdempotencyStore pour le contrat + l'invariant de scope de la clé.
17
+ */
18
+ declare class MemoryIdempotencyStore extends Service implements IIdempotencyStore {
19
+ /** Entrées vivantes — `null` tant qu'aucun `begin` n'a eu lieu (lazy alloc). */
20
+ private entries;
21
+ private readonly ttlMs;
22
+ private readonly leaseMs;
23
+ private readonly cap;
24
+ constructor(module: Module);
25
+ get size(): number;
26
+ /**
27
+ * {@inheritDoc IIdempotencyStore.listPage}
28
+ *
29
+ * La collection est déjà en RAM et **bornée par le cap** : le tri porte sur
30
+ * des références, seule la page devient des vues. Les entrées expirées sont
31
+ * exclues à la LECTURE (la purge d'ici est passive : elle n'a lieu qu'à
32
+ * l'écriture) — sinon on montrerait comme vivante une clé déjà rejouable.
33
+ */
34
+ listPage(query: IIdempotencyListQuery): Promise<IPage<IIdempotencyKeyEntry>>;
35
+ begin(key: string, fingerprint: string): IdempotencyOutcome;
36
+ complete(key: string, response: IdempotentResponse): void;
37
+ abort(key: string): void;
38
+ /**
39
+ * Borne la taille : purge passive des expirées rencontrées, puis éviction FIFO
40
+ * (la plus ancienne insérée) jusqu'à repasser sous le cap. Appelé au `complete`
41
+ * (seul point qui fait croître durablement la Map).
42
+ */
43
+ private evictIfNeeded;
44
+ }
45
+ export default MemoryIdempotencyStore;
@@ -0,0 +1,53 @@
1
+ import { Service, Module } from "nodefony";
2
+ import Route, { RouteOptions } from "../src/Route.js";
3
+ import { ContextType } from "@nodefony/http";
4
+ import Resolver from "../src/Resolver.js";
5
+ import Controller from "../src/Controller.js";
6
+ export type TypeController<T> = new (...args: any[]) => T;
7
+ declare class Router extends Service {
8
+ static routes: Route[];
9
+ routes: Route[];
10
+ private singletonControllers;
11
+ constructor(module: Module);
12
+ /**
13
+ * Retourne l'instance singleton d'une classe controller `@Scope("singleton")`,
14
+ * en la créant au premier appel via `create`. On cache la **promesse** (pas
15
+ * l'instance) : N requêtes concurrentes pendant la création (`initialize()`
16
+ * async) attendent le MÊME travail — jamais deux instances (race de création
17
+ * éliminée structurellement).
18
+ *
19
+ * @param ctor - la classe controller (clé du cache).
20
+ * @param create - fabrique exécutée une seule fois (instantiate + initialize).
21
+ * @returns la promesse de l'instance partagée.
22
+ */
23
+ getSingletonController(ctor: TypeController<Controller>, create: () => Promise<Controller>): Promise<Controller>;
24
+ /**
25
+ * Résout une route pour un contexte donné.
26
+ *
27
+ * @param context - le contexte HTTP/WS courant (porte container, méthode, URL…).
28
+ * @param cleanPathOverride - quand fourni, le matching se fait sur CE pathname au
29
+ * lieu de `context.request.url` — permet de router un path **porté par un message**
30
+ * (WS-RPC `invoke`) vers une action, sans muter l'URL de la connexion (état partagé).
31
+ * `undefined` (cas hot path normal) → comportement inchangé.
32
+ * @param methodOverride - méthode HTTP **logique** à exiger en plus du transport
33
+ * WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
34
+ * GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
35
+ * `undefined` (GET/HTTP) → match historique sur `context.method`.
36
+ * @returns un `Resolver` (`.resolve === true` si une route a matché).
37
+ */
38
+ resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): Resolver;
39
+ resolveController(contex: ContextType, name: string): Resolver;
40
+ matchRoutes(path: string): RegExpExecArray[];
41
+ getRoutes(name: string): Route[] | Route | undefined;
42
+ setRoute(): void;
43
+ removeRoutes(name: string): void;
44
+ static createRoute(name: string, obj: RouteOptions): Route;
45
+ static setController(myconstructor: TypeController<Controller>, module: Module): TypeController<Controller>;
46
+ /**
47
+ * Retourne les routes enregistrées pour un controller donné — utilisé par
48
+ * le décorateur `@controllers` pour logger chaque route depuis le module
49
+ * propriétaire (msgid `MODULE <name>` au lieu de `KERNEL`).
50
+ */
51
+ static getRoutesForController(myconstructor: TypeController<Controller>): Route[];
52
+ }
53
+ export default Router;
@@ -0,0 +1,193 @@
1
+ import { Service, Module, FileClass } from "nodefony";
2
+ import type { IController } from "../interfaces/index.js";
3
+ import Route from "./Route.js";
4
+ import { contextRequest, HTTPMethod, HttpResponse, Session, ContextType, Http2Response, WebsocketResponse } from "@nodefony/http";
5
+ import { OutgoingHttpHeaders } from "node:http";
6
+ import { ReadStream } from "node:fs";
7
+ import Eta from "../service/Eta.js";
8
+ type ReadStreamOptions = {
9
+ flags?: string;
10
+ encoding?: BufferEncoding;
11
+ fd?: number;
12
+ mode?: number;
13
+ autoClose?: boolean;
14
+ emitClose?: boolean;
15
+ start?: number;
16
+ end?: number;
17
+ highWaterMark?: number;
18
+ };
19
+ /**
20
+ * Parse un header `Range` mono-plage en octets (RFC 9110 §14.1.2).
21
+ *
22
+ * @param range - valeur brute du header `Range` (ex. `bytes=0-499`, `bytes=-500`).
23
+ * @param length - taille de la représentation sélectionnée (octets).
24
+ * @returns bornes `{ start, end }` clampées à la représentation,
25
+ * `"unsatisfiable"` si la plage est valide mais hors représentation (→ 416,
26
+ * RFC 9110 §15.5.17), ou `null` si le header doit être ignoré — unité ≠
27
+ * `bytes`, multi-range non supporté ou syntaxe invalide (RFC 9110 §14.2 :
28
+ * un serveur PEUT ignorer un Range ; on répond alors 200 complet, jamais 500).
29
+ */
30
+ export declare function parseByteRange(range: string, length: number): {
31
+ start: number;
32
+ end: number;
33
+ } | "unsatisfiable" | null;
34
+ /**
35
+ * Scope d'instanciation d'un controller (V4.3).
36
+ *
37
+ * - `"request"` (défaut) : une instance par requête — l'état per-request peut
38
+ * vivre sur `this` (legacy sûr, zéro breaking).
39
+ * - `"singleton"` (opt-in via `@Scope`) : UNE instance partagée par toutes les
40
+ * requêtes — réservé aux controllers **stateless** (état uniquement via
41
+ * arguments décorés + ALS). Un champ mutable par requête sur `this` y serait
42
+ * une data race silencieuse entre requêtes concurrentes.
43
+ */
44
+ export type ControllerScope = "request" | "singleton";
45
+ declare class Controller extends Service implements IController {
46
+ #private;
47
+ static prefix: string;
48
+ /**
49
+ * Scope d'instanciation de la classe — `"request"` par défaut, `"singleton"`
50
+ * posé par le décorateur `@Scope` (statique hérité, lu via `new.target` au
51
+ * constructor et par le Resolver : 0 Reflect). Cf {@link ControllerScope}.
52
+ */
53
+ static scope: ControllerScope;
54
+ module?: Module;
55
+ template?: Eta | null;
56
+ /**
57
+ * Contexte transport courant. Per-request : champ posé par `setContext`
58
+ * (constructor) — coût d'accès inchangé. Singleton stateless (V4.3) : champ
59
+ * jamais posé → lecture de l'ALS `RequestContext` (le `HttpKernel` y place
60
+ * le contexte à l'entrée du scope, V4.1) — chaque appel de helper retrouve
61
+ * LA requête en cours, jamais celle d'une requête concurrente.
62
+ */
63
+ get context(): ContextType | undefined;
64
+ set context(context: ContextType | undefined);
65
+ /**
66
+ * Route matchée. Per-request : posée par le Resolver via `setRoute`.
67
+ * Sans champ (singleton) : dérive du Resolver de la requête courante
68
+ * (`context.resolver`), donc toujours la route de CETTE requête.
69
+ */
70
+ get route(): Route | null;
71
+ get request(): contextRequest;
72
+ set request(request: contextRequest);
73
+ get response(): HttpResponse | Http2Response | WebsocketResponse | null;
74
+ set response(response: HttpResponse | Http2Response | WebsocketResponse | null);
75
+ get method(): HTTPMethod | undefined;
76
+ set method(method: HTTPMethod | undefined);
77
+ get queryGet(): Record<string, unknown>;
78
+ set queryGet(value: Record<string, unknown>);
79
+ get query(): Record<string, unknown>;
80
+ set query(value: Record<string, unknown>);
81
+ get queryFile(): unknown[];
82
+ set queryFile(value: unknown[]);
83
+ get queryPost(): Record<string, unknown>;
84
+ set queryPost(value: Record<string, unknown>);
85
+ /**
86
+ * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
87
+ * Fastify, NestJS), alias de {@link queryPost}.
88
+ *
89
+ * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
90
+ * corps. Pour une action typée, préférer le décorateur `@Body()`.
91
+ *
92
+ * Getter (aucune allocation) : `queryPost` reste la source unique.
93
+ */
94
+ get body(): Record<string, unknown>;
95
+ set body(value: Record<string, unknown>);
96
+ /**
97
+ * Session courante, ou `null`. Getter direct sur `context.session` (peuplé au
98
+ * point d'activation unique du pipeline si la route déclare `@UseSession` /
99
+ * `@Session`, ou si un cookie de session est repris — L1). Remplace l'ancien
100
+ * pont via l'event `onSessionStart` : toujours à jour, zéro allocation.
101
+ */
102
+ get session(): Session | null;
103
+ constructor(name: string, context: ContextType);
104
+ setContext(context: ContextType): void;
105
+ setContextJson(encoding?: BufferEncoding): void | undefined;
106
+ setContextHtml(encoding?: BufferEncoding): void | undefined;
107
+ render(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: Record<string, string | number>): Promise<Http2Response | HttpResponse>;
108
+ renderResponse(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: OutgoingHttpHeaders): Promise<Http2Response | HttpResponse> | Promise<WebsocketResponse>;
109
+ renderView(path: string | FileClass, param?: Record<string, unknown>, status?: string | number, headers?: Record<string, string | number>): Promise<Http2Response | HttpResponse | WebsocketResponse>;
110
+ /**
111
+ * Injecte les helpers frontend (`frontendTags`/`frontendDocument`) dans les
112
+ * locals du template (passés en data, pas de registre global de fonctions).
113
+ * Service `frontend` résolu par nom (pas d'import `@nodefony/frontend`). Les
114
+ * valeurs fournies par l'action priment (spread `param` en dernier).
115
+ */
116
+ private withFrontendLocals;
117
+ renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): Promise<Http2Response | HttpResponse | WebsocketResponse>;
118
+ setRoute(route: Route): Route;
119
+ getSession(): Session | undefined | null;
120
+ redirect(url: string, status?: string | number, headers?: Record<string, string | number>): void;
121
+ getFlashBag(key: string): unknown;
122
+ setFlashBag(key: string, value: unknown): unknown;
123
+ addFlash(key: string, value: unknown): unknown;
124
+ forward(name: string, param?: unknown[]): Promise<unknown>;
125
+ /**
126
+ * @deprecated Bloque l'event-loop (`fs.lstatSync` via `new FileClass`).
127
+ * Utiliser {@link getFileAsync} dans tout pipeline. Conservé pour compat.
128
+ */
129
+ getFile(file: FileClass | string): FileClass;
130
+ /**
131
+ * Variante **async** de `getFile()` — résout les stats via `FileClass.from`
132
+ * (pas de `lstatSync` bloquant). À préférer dans le pipeline (render/stream).
133
+ *
134
+ * Un chemin qui ne désigne aucun fichier rend **404**, pas 500 : la RFC 9110
135
+ * §15.5.5 définit le 404 comme l'absence de « représentation courante pour la
136
+ * ressource cible », alors que le 500 (§15.6.1) suppose une condition
137
+ * **inattendue**. Or le chemin vient presque toujours d'un paramètre d'URL :
138
+ * un nom qui ne correspond à rien est une entrée client banale, pas une panne.
139
+ * Un dossier reçoit le même traitement — il n'est pas servable comme fichier.
140
+ *
141
+ * Les autres échecs d'accès (permissions, E/S) remontent INCHANGÉS et valent
142
+ * 500 : là, la condition est bien inattendue. Le 403 ne conviendrait pas, la
143
+ * RFC le réservant à un refus « compris et délibéré » (§15.5.4).
144
+ *
145
+ * Le message rendu au client ne cite JAMAIS le chemin — la même section
146
+ * autorise le serveur à ne pas divulguer l'existence d'une ressource, et un
147
+ * chemin serveur dans un corps d'erreur est une fuite d'information. Le chemin
148
+ * va dans le journal, à la disposition de qui exploite l'application.
149
+ *
150
+ * @param file - `FileClass` déjà hydraté OU chemin string.
151
+ * @returns le `FileClass` (type `"File"` validé).
152
+ * @throws {HttpError} 404 si le chemin ne désigne aucun fichier.
153
+ * @throws Si le type de l'argument est invalide (bug d'appel, pas d'entrée client).
154
+ */
155
+ getFileAsync(file: FileClass | string): Promise<FileClass>;
156
+ /**
157
+ * Sert un fichier en TÉLÉCHARGEMENT (`Content-Disposition: attachment`).
158
+ *
159
+ * Comme {@link Controller.streamFile} dont il est l'habillage, il envoie le
160
+ * fichier ENTIER et n'honore pas `Range` — c'est le comportement voulu pour un
161
+ * téléchargement. Pour un média seekable, voir {@link Controller.renderMediaStream}.
162
+ *
163
+ * @param file - chemin ou `FileClass` du fichier à servir.
164
+ * @param options - options de `createReadStream`.
165
+ * @param headers - en-têtes ajoutés (écrasent ceux posés par défaut).
166
+ * @returns le flux de lecture, une fois la réponse écoulée.
167
+ */
168
+ renderFileDownload(file: FileClass | string, options?: ReadStreamOptions, headers?: OutgoingHttpHeaders): Promise<ReadStream>;
169
+ /**
170
+ * Envoie un fichier en FLUX, du disque vers la réponse, sans jamais le charger
171
+ * en mémoire.
172
+ *
173
+ * ⚠️ **N'honore PAS l'en-tête `Range`** : le fichier part toujours en entier,
174
+ * avec un statut 200. Pour un média dans lequel un lecteur doit pouvoir sauter
175
+ * (vidéo, audio, gros PDF), utiliser {@link Controller.renderMediaStream} —
176
+ * seul à implémenter les requêtes par plage (RFC 9110 §14 : 206, 416,
177
+ * `Content-Range`). Annoncer `Accept-Ranges: bytes` depuis ici est un piège :
178
+ * le client croit pouvoir se déplacer et reçoit tout le fichier.
179
+ *
180
+ * Ce que cette méthode garantit et qui justifie son existence : le NETTOYAGE.
181
+ * Le flux est ouvert en `autoClose: false` ; un client qui raccroche en plein
182
+ * téléchargement laisserait sinon un descripteur ouvert et une promesse pendue.
183
+ *
184
+ * @param file - chemin ou `FileClass` du fichier à servir.
185
+ * @param headers - en-têtes ajoutés à la réponse (le type MIME est déduit si absent).
186
+ * @param options - options de `createReadStream` (`start`/`end` pour un extrait).
187
+ * @returns le flux de lecture, une fois la réponse écoulée.
188
+ * @throws Si la réponse n'est pas disponible, ou si le fichier est illisible.
189
+ */
190
+ streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions | undefined): Promise<ReadStream>;
191
+ renderMediaStream(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions | undefined): Promise<HttpResponse | ReadStream | WebsocketResponse>;
192
+ }
193
+ export default Controller;
@@ -0,0 +1,35 @@
1
+ import type { IAdminApi } from "nodefony";
2
+ import type { IAdminBroker } from "../interfaces/IAdminBroker.js";
3
+ /** Options de composition du producteur framework. */
4
+ export interface FrameworkAdminApiOptions {
5
+ /**
6
+ * Monte les endpoints Playground (`playground/routes`) — **dev uniquement** :
7
+ * la console Studio exécute des mutations depuis le navigateur. Hors dev les
8
+ * endpoints n'existent pas (404) → la page Studio affiche « dev uniquement ».
9
+ */
10
+ playground?: boolean;
11
+ }
12
+ /**
13
+ * Producteur `IAdminApi` du module **framework** — exposé sous
14
+ * `/nodefony/framework/api/*`.
15
+ *
16
+ * 3ᵉ producteur du data plane admin (kernel, http, puis framework). Introspecte
17
+ * le **Router** : c'est l'équivalent web de `nodefony router:dump` et la source
18
+ * de la future vue « Routes » de Studio (P10.8).
19
+ *
20
+ * Le framework héberge le broker → il s'enregistre directement (pas besoin de
21
+ * passer par `IAdminRegistry` du container comme un module externe).
22
+ *
23
+ * Endpoints :
24
+ * - `GET /nodefony/framework/api/routes` → toutes les routes enregistrées
25
+ * - `GET /nodefony/framework/api/info` → résumé (nb routes, méthodes, modules)
26
+ * - `GET /nodefony/framework/api/admin` → **catalogue** du data plane admin
27
+ * (tous les producteurs + descriptors + endpoints) — pièce « discovery »
28
+ * de P10.2 que Studio lit pour générer sa navigation admin.
29
+ *
30
+ * @param broker - le broker admin (pour le catalogue). Optionnel : sans lui,
31
+ * `admin` renvoie une liste vide.
32
+ * @param opts - composition (`playground: true` → endpoints Playground, dev-only).
33
+ * @returns le contrat admin de framework, prêt à `broker.register()`.
34
+ */
35
+ export declare function createFrameworkAdminApi(broker?: IAdminBroker, opts?: FrameworkAdminApiOptions): IAdminApi;
@@ -0,0 +1,71 @@
1
+ import type { IKernel, IAdminApi } from "nodefony";
2
+ /**
3
+ * Sérialisation défensive de config : borne la profondeur, neutralise les
4
+ * fonctions, casse les cycles, et **relativise les chemins absolus** (sécu :
5
+ * ne jamais exposer l'arborescence serveur). Les `options` d'un module peuvent
6
+ * contenir des fonctions/refs circulaires (vers le kernel) → JSON.stringify
7
+ * direct planterait.
8
+ */
9
+ export declare function safeConfig(value: unknown, depth?: number, seen?: WeakSet<object>): unknown;
10
+ /** Entrée config d'un module : valeurs redactées + schéma + provenance par champ. */
11
+ export interface IConfigEntry {
12
+ /** Clé Studio (basename : `http`, `security`, `core`, ou la clé de l'app). */
13
+ key: string;
14
+ /** Nom de package (`@nodefony/http`, nom de l'app…). */
15
+ name: string;
16
+ /** Est-ce la config de l'APPLICATION (vs un module) ? */
17
+ isApp: boolean;
18
+ /** Segment d'adressage des overrides (`NF__<SEG>__…`) : `app` ou le basename. */
19
+ seg: string;
20
+ /** Config effective résolue, secrets REDACTÉS côté serveur. */
21
+ config: Record<string, unknown>;
22
+ /** JSON Schema du module (si migré Zod), sinon `null`. */
23
+ configSchema: unknown;
24
+ /** Origine par champ (`default`/`app`/`env`) — `null` si pas de schéma. */
25
+ provenance: Record<string, string> | null;
26
+ /**
27
+ * « QUI surcharge, où » par champ env-surchargé : chemin pointé → **nom RÉEL**
28
+ * de la variable d'environnement actuellement posée (`NF__SECURITY__JWT__ACCESSTTLS`).
29
+ * Permet à Studio de nommer la source exacte (≠ recette générique).
30
+ */
31
+ envKeys: Record<string, string>;
32
+ /**
33
+ * « QUI surcharge, où » par champ **app**-surchargé : chemin pointé → SOURCE
34
+ * réelle. Soit un MODULE qui reconfigure celui-ci via `module-<seg>` (cross-module,
35
+ * ex. `@nodefony/test` qui surcharge `http.upload.maxFileSize`), soit
36
+ * `nodefony.config.ts` (config app directe / `use()`). Rempli par
37
+ * {@link attributeOverrideSources} (a besoin de TOUS les modules → côté agrégat).
38
+ */
39
+ overriddenBy: Record<string, string>;
40
+ }
41
+ /**
42
+ * Paquets à essayer pour une clé de module, **dans l'ordre**, et bornés au
43
+ * périmètre du framework.
44
+ *
45
+ * Pure — donc éprouvable sans disque ni dossier courant, et c'est nécessaire :
46
+ * les deux défauts qu'elle corrige tiennent l'un à l'ORDRE, l'autre au
47
+ * PÉRIMÈTRE, jamais au système de fichiers.
48
+ *
49
+ * 🔴 **Le scope d'abord.** `redis` désigne ici le module Nodefony — mais un
50
+ * client Redis tiers du même nom vit dans le même `node_modules`. L'essayer en
51
+ * premier le faisait gagner : le paquet trouvé n'avait pas de documentation,
52
+ * la réponse sortait VIDE, et le cas exact qu'on venait de corriger était le
53
+ * seul à rater. Un nom court est une clé Nodefony avant d'être un nom npm ;
54
+ * l'homonyme tiers ne vient qu'après.
55
+ *
56
+ * 🔴 **Le périmètre n'est pas la traversée.** {@link PACKAGE_NAME} empêche
57
+ * `../../etc` de désigner un dossier hors de l'arbre — elle ne dit rien de ce
58
+ * qu'on a le droit de servir. Sans cette seconde garde, la porte de
59
+ * documentation rendait les pages de n'importe quelle dépendance installée
60
+ * (`chrome-launcher` en a), c'est-à-dire qu'elle exposait l'arbre de
61
+ * dépendances d'une application à qui interroge la porte.
62
+ *
63
+ * ⚠️ `nodefony` en toutes lettres : le socle se nomme ainsi sur npm (héritage
64
+ * du dépôt JS) quand le reste de la pile porte le scope ; `CORE_PACKAGE` est
65
+ * son nom LOGIQUE, pas celui du dossier installé.
66
+ *
67
+ * @param name - clé courte (`redis`) ou nom de paquet (`@nodefony/redis`).
68
+ * @returns les noms de paquets à tenter, du plus probable au moins probable.
69
+ */
70
+ export declare function nodefonyPackageCandidates(name: string): string[];
71
+ export declare function createKernelAdminApi(kernel: IKernel): IAdminApi;