@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,369 @@
1
+ /**
2
+ * Frontmatter d'un fichier de doc module. Tous les champs sont optionnels —
3
+ * un `.md` sans frontmatter reste lisible (titre dérivé du premier `# H1`).
4
+ *
5
+ * On accepte les alias historiques (`last-updated` ⇄ `updated`, `topic` ⇄
6
+ * `title`) pour ne pas réécrire les docs `architecture/*` déjà frontmattées.
7
+ */
8
+ export interface DocFrontmatter {
9
+ title?: string;
10
+ navTitle?: string;
11
+ module?: string;
12
+ since?: string;
13
+ updated?: string;
14
+ status?: string;
15
+ order?: number;
16
+ [key: string]: unknown;
17
+ }
18
+ /** Entrée du sommaire docs d'un module (sans le corps markdown). */
19
+ export interface DocSummary {
20
+ /** Identifiant url-safe = nom de fichier sans extension. */
21
+ slug: string;
22
+ /** Titre humain (frontmatter `title`/`topic`, sinon premier H1, sinon slug). */
23
+ title: string;
24
+ /**
25
+ * Libellé COURT du menu (frontmatter `navTitle`), repli sur {@link title}. Un
26
+ * titre est écrit pour être lu en tête d'article ; une colonne de navigation
27
+ * n'a pas la même largeur, et une recherche doit trouver le mot AFFICHÉ.
28
+ */
29
+ navTitle: string;
30
+ /** `draft` | `stable` | `deprecated` | `null`. */
31
+ status: string | null;
32
+ since: string | null;
33
+ /** `updated`/`last-updated` du frontmatter. */
34
+ updated: string | null;
35
+ /** Date ISO du dernier commit git du fichier (dérive doc↔code). `null` hors git. */
36
+ gitUpdated: string | null;
37
+ /** Ordre d'affichage croissant (frontmatter `order`, défaut 100, `index`=0). */
38
+ order: number;
39
+ }
40
+ /** Doc complète : frontmatter + corps markdown (frontmatter retiré). */
41
+ export interface DocContent {
42
+ slug: string;
43
+ frontmatter: DocFrontmatter;
44
+ markdown: string;
45
+ gitUpdated: string | null;
46
+ }
47
+ /** Symbole TS exporté d'un module, projeté depuis `.ai/symbols.json`. */
48
+ export interface ModuleSymbol {
49
+ name: string;
50
+ kind: string;
51
+ file: string;
52
+ description: string | null;
53
+ extends: string | null;
54
+ implements: string[];
55
+ decorators: string[];
56
+ }
57
+ /**
58
+ * Parse un bloc frontmatter YAML minimaliste (clé: valeur scalaires).
59
+ *
60
+ * Volontairement sans dépendance (`gray-matter` etc.) : on ne gère que des
61
+ * scalaires `key: value` entre deux `---`. Les listes inline (`[a, b]`) sont
62
+ * conservées en string brute. Suffisant pour `title/module/status/since/
63
+ * updated/order`. Renvoie `{ data, body }` ; sans fence → `data` vide.
64
+ */
65
+ export declare function parseFrontmatter(raw: string): {
66
+ data: DocFrontmatter;
67
+ body: string;
68
+ };
69
+ /**
70
+ * Sommaire des docs d'un module : lit les `*.md` de `<modulePath>/docs/`
71
+ * (un niveau, non récursif), parse le frontmatter, trie par `order` puis slug.
72
+ *
73
+ * @param modulePath - chemin disque du module (`Module.path`).
74
+ * @returns liste triée (vide si le dossier `docs/` est absent).
75
+ */
76
+ export declare function listModuleDocs(modulePath: string, withGit?: boolean): Promise<DocSummary[]>;
77
+ /**
78
+ * Comptage rapide des docs (`*.md`) d'un module — readdir seul, sans lire/parser
79
+ * ni `git`. Pour les KPI/overview (`docsCount`) où seul le nombre importe.
80
+ *
81
+ * @param modulePath - chemin disque du module (`Module.path`).
82
+ * @returns nombre de fichiers `.md` dans `<modulePath>/docs/` (0 si absent).
83
+ */
84
+ export declare function countModuleDocs(modulePath: string): Promise<number>;
85
+ /**
86
+ * Lit une doc module par slug : frontmatter + corps markdown brut.
87
+ *
88
+ * Le slug est borné (`SLUG_RE`) avant toute jointure de chemin → pas de path
89
+ * traversal vers l'extérieur de `<modulePath>/docs/`.
90
+ *
91
+ * @returns `null` si slug invalide ou fichier absent.
92
+ */
93
+ export declare function readModuleDoc(modulePath: string, slug: string): Promise<DocContent | null>;
94
+ /** Où un terme cherché apparaît, et la ligne qui le porte. */
95
+ export interface DocSearchMatch {
96
+ /** Ligne (1-indexée) dans le CORPS markdown, frontmatter retiré. */
97
+ line: number;
98
+ /** La ligne, ramenée à une fenêtre lisible autour du terme. */
99
+ text: string;
100
+ }
101
+ /** Une doc qui répond à la recherche, avec ses extraits. */
102
+ export interface DocSearchHit {
103
+ /** Clé du module qui porte la doc (`http`, `security`, `core`…). */
104
+ module: string;
105
+ slug: string;
106
+ title: string;
107
+ /** Extraits, dans l'ordre du document — bornés (cf `perDoc`). */
108
+ matches: DocSearchMatch[];
109
+ /** Occurrences TOTALES dans la doc ; les extraits, eux, sont bornés. */
110
+ occurrences: number;
111
+ /** Pertinence décroissante — un titre ou un slug porteur du terme pèse. */
112
+ score: number;
113
+ }
114
+ /** Une doc à balayer : la clé du module qui la porte, et son chemin disque. */
115
+ export interface DocSearchTarget {
116
+ /** Clé d'affichage du module (`http`, `security`, `core`…). */
117
+ key: string;
118
+ /** Chemin disque du module (`Module.path`). */
119
+ path: string;
120
+ }
121
+ /** Ce que rend une recherche : les docs retenues, et ce qu'elle a balayé. */
122
+ export interface DocSearchResult {
123
+ /** Termes effectivement cherchés, après normalisation. */
124
+ terms: string[];
125
+ /** Docs balayées — dit à l'agent si le corpus était bien là. */
126
+ scanned: number;
127
+ /** Docs qui portent TOUS les termes — avant la borne d'affichage. */
128
+ matched: number;
129
+ /** Les meilleures, bornées par `limit`. */
130
+ hits: DocSearchHit[];
131
+ /**
132
+ * Phrase d'annonce, présente UNIQUEMENT quand la borne a joué.
133
+ *
134
+ * ⚠️ `matched` seul ne suffit pas à s'en apercevoir : quand le nombre de
135
+ * documents trouvés égale la borne — le cas exact d'un corpus fourni — un
136
+ * lecteur voit « 20 » des deux côtés et conclut qu'il tient tout. Il faut
137
+ * donc le DIRE, et nommer le geste qui donne la suite. Absente quand tout
138
+ * est rendu : annoncer une coupe qui n'a pas eu lieu ferait chercher un
139
+ * reste inexistant.
140
+ */
141
+ note?: string;
142
+ }
143
+ /**
144
+ * Cherche un texte dans les docs colocalisées des modules donnés.
145
+ *
146
+ * ⭐ **Pourquoi cette fonction existe** : chez un utilisateur, la documentation
147
+ * des modules vit sous `node_modules/@nodefony/<mod>/docs/` — un dossier que `git`
148
+ * ignore, donc que `rg` et les outils de recherche des agents EXCLUENT par
149
+ * défaut. La doc est livrée et introuvable ; c'est cette porte qui la rend
150
+ * atteignable, sans quoi l'agent réécrit à la main ce qui est déjà écrit.
151
+ *
152
+ * Un document est retenu s'il porte **TOUS** les termes (et non l'un d'eux) :
153
+ * sur un corpus où « session » apparaît partout, un OU rendrait le corpus.
154
+ * La comparaison est faite sans casse ni diacritiques ({@link fold}).
155
+ *
156
+ * Aucun index n'est conservé : le corpus est relu à chaque appel. C'est une
157
+ * opération de développement, rare et explicite — un index en mémoire coûterait
158
+ * en permanence ce qu'il ferait gagner quelques fois, et se périmerait à la
159
+ * première doc éditée.
160
+ *
161
+ * @param targets - modules à balayer (clé + chemin disque)
162
+ * @param query - texte cherché ; les espaces séparent des termes cumulatifs
163
+ * @param options - bornes de rendu (`limit` docs, `perDoc` extraits)
164
+ * @returns les docs retenues, et ce que la recherche a réellement balayé
165
+ */
166
+ export declare function searchModuleDocs(targets: readonly DocSearchTarget[], query: string, options?: {
167
+ limit?: number;
168
+ perDoc?: number;
169
+ }): Promise<DocSearchResult>;
170
+ /**
171
+ * Symboles TS exportés d'un module + descriptions TSDoc, lus depuis
172
+ * `.ai/symbols.json` (à la racine de l'application, cf {@link appRoot}).
173
+ *
174
+ * 100 % auto-généré (jamais de `.d.ts` manuel) → zéro divergence avec le code.
175
+ * Tab API maigre tant que la couverture TSDoc est faible : c'est volontaire,
176
+ * ça pousse à documenter.
177
+ *
178
+ * @param packageName - nom npm du module (`Module.getModuleName()`, ex
179
+ * `"@nodefony/http"`) — clé `.module` dans le graphe symbolique.
180
+ * @returns symboles exportés triés par kind puis nom (vide si fichier absent).
181
+ */
182
+ export declare function listModuleSymbols(packageName: string): Promise<ModuleSymbol[]>;
183
+ /** Ce qu'on a trouvé d'un symbole dans les types LIVRÉS d'un module. */
184
+ export interface SymbolDeclaration {
185
+ /**
186
+ * Fichier de TYPES qui porte la déclaration, relatif au module — jamais
187
+ * absolu (une réponse ne publie pas l'arborescence du serveur).
188
+ *
189
+ * Nommé `declarationFile` et non `file` : le graphe symbolique porte DÉJÀ un
190
+ * `file`, qui désigne la SOURCE dans le dépôt d'origine — un chemin qui
191
+ * n'existe pas chez celui qui a installé le paquet. Deux notions sous un même
192
+ * nom, et la fusion des deux réponses en écrasait une.
193
+ */
194
+ declarationFile: string;
195
+ /** Le bloc de déclaration, TSDoc compris. */
196
+ declaration: string;
197
+ /** Le bloc a-t-il été coupé ? Une troncature muette vaut un mensonge. */
198
+ truncated: boolean;
199
+ }
200
+ /**
201
+ * Extrait la déclaration d'un symbole des types LIVRÉS d'un module.
202
+ *
203
+ * ⭐ **Pourquoi cette fonction existe** : le graphe symbolique
204
+ * (`.ai/symbols.json`) dit qu'un symbole existe, ce qu'il étend et la première
205
+ * phrase de sa documentation — mais **pas sa signature**. À « quels arguments
206
+ * prend cette méthode ? », un agent n'a donc aucune réponse : les `.d.ts` qui
207
+ * la portent vivent sous `node_modules`, que git ignore et que les outils de
208
+ * recherche de fichiers excluent. Il devine, et il devine faux.
209
+ *
210
+ * Le nom demandé ne touche JAMAIS un chemin : il sert à filtrer du texte. Les
211
+ * fichiers balayés sont dérivés du module, pas de l'appelant — un paramètre qui
212
+ * entrerait dans un `join` serait une traversée de répertoire offerte.
213
+ *
214
+ * @param modulePath - chemin disque du module (`Module.path`)
215
+ * @param symbolName - nom exact du symbole (`AbstractCrudService`, `IKernel`)
216
+ * @returns la déclaration et son fichier, ou `null` si rien ne la porte
217
+ */
218
+ export declare function readSymbolDeclaration(modulePath: string, symbolName: string): Promise<SymbolDeclaration | null>;
219
+ /**
220
+ * Identité du package npm du core (`@nodefony/core`) tel qu'indexé dans
221
+ * `.ai/symbols.json` — le core est référencé sous ce nom logique, **pas** sous
222
+ * son nom npm réel (`nodefony`, héritage JS).
223
+ */
224
+ export declare const CORE_PACKAGE = "@nodefony/core";
225
+ /** Dépendance d'un module : range déclarée + version installée. */
226
+ export interface DepInfo {
227
+ name: string;
228
+ kind: "nodefony" | "external";
229
+ range: string | null;
230
+ installed: string | null;
231
+ }
232
+ /** Statut "outdated" d'une dep externe (registry npm). */
233
+ export interface OutdatedInfo {
234
+ name: string;
235
+ installed: string | null;
236
+ latest: string | null;
237
+ outdated: boolean;
238
+ }
239
+ /**
240
+ * Dépendances d'un module : depuis son `package.json` (dependencies +
241
+ * peerDependencies) avec la version RÉELLEMENT installée (lue dans
242
+ * `node_modules/<dep>/package.json`, local puis hoisté à la racine).
243
+ */
244
+ export declare function readDependencies(modulePath: string): Promise<DepInfo[]>;
245
+ /**
246
+ * Vérifie les MAJ des deps EXTERNES via le registry npm (`/<pkg>/latest`).
247
+ * Les deps Nodefony (workspaces locaux) sont ignorées. Réseau → on-demand.
248
+ */
249
+ export declare function checkOutdated(deps: DepInfo[]): Promise<OutdatedInfo[]>;
250
+ /** Résultat d'un lancement de tests (un fichier ou toute la suite). */
251
+ export interface TestRunResult {
252
+ ok: boolean;
253
+ code: number | null;
254
+ passed: number;
255
+ failed: number;
256
+ durationMs: number;
257
+ output: string;
258
+ mode: string;
259
+ }
260
+ /**
261
+ * Liste les fichiers de test **unit** d'un module (lançables par `vitest run`).
262
+ * Si aucune suite unit, repli sur tous les fichiers. Chemins relatifs au module.
263
+ */
264
+ export declare function listTestFiles(modulePath: string): Promise<string[]>;
265
+ /** Un groupe de suites de tests (pour l'onglet Tests de Studio). */
266
+ export interface TestGroup {
267
+ /** Catégorie (`unit`/`integration`/`e2e`/`load`/`websockets`/`routing`/`memory`/`autre`). */
268
+ category: string;
269
+ files: string[];
270
+ /** Lançable depuis Studio ? (unit seulement — les autres tapent un serveur/DB). */
271
+ runnable: boolean;
272
+ }
273
+ /**
274
+ * Groupe TOUS les fichiers de test d'un module par catégorie (lecture seule pour
275
+ * l'onglet Tests — les non-unit ne sont pas lançables depuis Studio : ils exigent
276
+ * un serveur live / une base, donc `runnable:false`). Donne une vue complète des
277
+ * suites (intégration/e2e/charge/mémoire) qui étaient invisibles auparavant.
278
+ */
279
+ export declare function listTestGroups(modulePath: string): Promise<TestGroup[]>;
280
+ /** Commande exacte que `runModuleTests` lance — pure, testable sans process. */
281
+ export interface TestRunCommand {
282
+ cmd: string;
283
+ args: string[];
284
+ mode: string;
285
+ }
286
+ /**
287
+ * Compose la commande d'un run de tests de module, sans l'exécuter.
288
+ *
289
+ * - 1 fichier (module vitest) → `npx vitest run <file>` : le chemin est un
290
+ * FILTRE positionnel de vitest. Il ne doit JAMAIS suivre un `--` : vitest
291
+ * ignore tout ce qui vient après, et la suite ENTIÈRE tourne en silence
292
+ * (vécu : 18 fichiers joués pour un seul demandé, verdict « vert »). La
293
+ * défense anti-injection d'argument est la garde de l'appelant (pas de
294
+ * préfixe `-`, pas de `..`, suffixe `.test.ts` — cf KernelAdminApi).
295
+ * - sinon (module vitest) → run-all, reporters coverage FORCÉS vers
296
+ * `.coverage` : l'onglet Coverage de Studio apparaît quelle que soit la
297
+ * config du module (la liste `reporter` dupliquée par module divergeait).
298
+ * - sinon (cœur : monocart, pas de `vitest.config.ts`) → `npm run coverage`.
299
+ *
300
+ * @param modulePath - racine du module (où vit `vitest.config.ts`)
301
+ * @param file - chemin relatif d'UN fichier de test, déjà validé par l'appelant
302
+ * @returns commande, arguments en tableau (spawn sans shell) et libellé du mode
303
+ */
304
+ export declare function testRunCommand(modulePath: string, file?: string): TestRunCommand;
305
+ /**
306
+ * Lance les tests d'un module et renvoie un résumé (pass/fail/durée + tail).
307
+ *
308
+ * - 1 fichier (module vitest) → `npx vitest run <file>` (rapide, pass/fail).
309
+ * - sinon → `npm run coverage` (suite complète + refresh coverage ; marche
310
+ * pour vitest comme pour monocart/core).
311
+ *
312
+ * ⚠️ EXÉCUTE un process — appelé UNIQUEMENT derrière le garde dev-only de
313
+ * l'endpoint (cf KernelAdminApi). spawn sans shell + args en tableau (pas
314
+ * d'injection shell). `file` validé en amont (suffixe .test.ts, pas de `..`).
315
+ */
316
+ export declare function runModuleTests(modulePath: string, file?: string): Promise<TestRunResult>;
317
+ /**
318
+ * Chemin disque racine du package core (`nodefony`).
319
+ *
320
+ * Le core n'est **pas** un module chargé (`kernel.getModules()` n'a pas de clé
321
+ * `core`) : c'est le socle de tous les autres. On résout donc son emplacement
322
+ * pour lire ses docs colocalisées (`<core>/docs/*.md`).
323
+ *
324
+ * Dev self-hosted : `<racine de l'app>/src/nodefony`. Fallback prod :
325
+ * résolution du package npm `nodefony` (remontée jusqu'à son `package.json`).
326
+ */
327
+ export declare function resolveCorePath(): string;
328
+ /** Couverture d'un fichier (pourcentages, format json-summary istanbul/v8). */
329
+ export interface CoverageFile {
330
+ file: string;
331
+ lines: number;
332
+ statements: number;
333
+ functions: number;
334
+ branches: number;
335
+ }
336
+ /** Rapport de couverture d'un module pour Studio (onglet Coverage). */
337
+ export interface CoverageReport {
338
+ available: boolean;
339
+ generated?: string | null;
340
+ total?: {
341
+ lines: number;
342
+ statements: number;
343
+ functions: number;
344
+ branches: number;
345
+ };
346
+ files?: CoverageFile[];
347
+ }
348
+ /**
349
+ * Lit le dernier rapport de couverture d'un module dans `<module>/.coverage/`.
350
+ * Préfère `coverage-summary.json` (vitest, a les statements) ; sinon parse
351
+ * `lcov.info` (produit par monocart côté core ET par vitest). Studio AFFICHE ce
352
+ * rapport — il ne lance pas les tests. `available:false` si rien de généré.
353
+ */
354
+ export declare function readCoverage(modulePath: string): Promise<CoverageReport>;
355
+ /** Descripteur du pseudo-module `core` pour la carte/détail Studio. */
356
+ export interface CoreInfo {
357
+ path: string;
358
+ name: string;
359
+ version: string | null;
360
+ dependencies: string[];
361
+ }
362
+ /**
363
+ * Métadonnées du core pour Studio (carte + onglet Vue d'ensemble).
364
+ *
365
+ * `name` est forcé à `@nodefony/core` (cohérent avec `.ai/symbols.json` et le
366
+ * frontmatter `module:`), même si son `package.json` se nomme `nodefony`.
367
+ * `version`/`dependencies` viennent de ce `package.json`.
368
+ */
369
+ export declare function readCoreInfo(): Promise<CoreInfo>;
@@ -0,0 +1,96 @@
1
+ import type { IIdempotencyStore, IdempotentResponse } from "nodefony";
2
+ /** `true` si la méthode est une mutation éligible à l'idempotence. */
3
+ export declare function isMutationMethod(method: string | null | undefined): boolean;
4
+ /**
5
+ * Borne d'une clé d'idempotence (convention Stripe). Une clé est un identifiant
6
+ * court (UUID) ; au-delà, elle est traitée comme ABSENTE (anti-DoS du cache borné).
7
+ */
8
+ export declare const IDEMPOTENCY_KEY_MAX = 255;
9
+ /**
10
+ * Verdict **neutre** rendu par {@link evaluateIdempotency} — décrit QUOI faire
11
+ * sans rien savoir du transport :
12
+ * - `execute` : pas de clé en mode souple, ou store/identité absents → exécuter
13
+ * SANS mémoriser (jamais de partage cross-identité).
14
+ * - `guarded` : clé fraîche réservée *in-flight* → exécuter PUIS `complete(key)`
15
+ * (succès) ou `abort(key)` (échec), `key` = clé scopée à réutiliser telle quelle.
16
+ * - `replay` : rejeu d'une mutation déjà complétée → renvoyer `response` SANS
17
+ * exécuter.
18
+ * - `reject` : court-circuit d'erreur (400 clé requise / 409 concurrent / 422
19
+ * clé réutilisée avec un autre payload).
20
+ */
21
+ export type IdempotencyVerdict = {
22
+ kind: "execute";
23
+ } | {
24
+ kind: "guarded";
25
+ key: string;
26
+ } | {
27
+ kind: "replay";
28
+ response: IdempotentResponse;
29
+ } | {
30
+ kind: "reject";
31
+ status: 400 | 409 | 422;
32
+ message: string;
33
+ detail?: string;
34
+ };
35
+ /**
36
+ * Résout la clé d'idempotence d'une requête : posée dans l'ALS par le pont WS
37
+ * (`als.idempotencyKey`) ou lue de l'en-tête HTTP `Idempotency-Key` (clé
38
+ * minuscule côté Node, éventuellement répétée → premier élément). L'ALS prime sur
39
+ * l'en-tête. Une clé > {@link IDEMPOTENCY_KEY_MAX} est traitée comme **absente**
40
+ * (anti-DoS) plutôt que stockée. `undefined` si rien d'exploitable.
41
+ */
42
+ export declare function resolveIdempotencyKey(alsKey: unknown, header: unknown): string | undefined;
43
+ /**
44
+ * Identité stable pour **scoper** le cache d'idempotence (anti-IDOR : un
45
+ * utilisateur ne doit jamais rejouer la clé d'un autre). Dérivée de l'utilisateur
46
+ * (`username` / `identifier` / `id`, sans coupler le framework au contrat `IUser`),
47
+ * avec fallback sur l'`userId` de l'ALS. `null` = pas d'identité fiable → l'appelant
48
+ * n'utilise PAS le cache (jamais de partage cross-identité).
49
+ *
50
+ * ⚠️ Doit renvoyer la MÊME valeur quel que soit le transport pour un même compte
51
+ * (sinon une mutation tentée en WS puis rejouée en HTTP ne dédoublonnerait pas).
52
+ * D'où la dérivation de `request.user` (posé UNIFORMÉMENT dans l'ALS par les deux
53
+ * transports), et NON de `getUserId()` seul (le firewall HTTP ne le pose pas
54
+ * toujours — vécu S4).
55
+ */
56
+ export declare function resolveIdentity(user: unknown): string | null;
57
+ /**
58
+ * Empreinte SHA-256 du **payload** d'une requête (parties sérialisables : nom de
59
+ * route + params + corps). Comparée par le store à l'empreinte mémorisée pour une
60
+ * clé : si elle diffère, la clé est réutilisée pour une AUTRE requête → 422
61
+ * (draft §2.4). Hash → empreinte courte (anti-DoS mémoire) + comparaison O(1).
62
+ */
63
+ export declare function computeFingerprint(parts: unknown): string;
64
+ /**
65
+ * Cœur normatif : à partir de la clé client, de l'identité, du fingerprint et du
66
+ * transport, rend le {@link IdempotencyVerdict} à appliquer. La réservation
67
+ * (`store.begin`) est **atomique** (mono-thread JS côté mémoire, `SET … NX` côté
68
+ * Redis) ; le store peut être sync (mémoire) ou async (distribué) → `begin` est
69
+ * `await`é, d'où le retour `Promise<IdempotencyVerdict>`.
70
+ *
71
+ * Le `required` **effectif** = `required || isWs` : une mutation par socket exige
72
+ * TOUJOURS une clé (le WS reconnecte/rejoue → muter sans garde-fou exposerait au
73
+ * double-effet), tandis qu'en HTTP la clé n'est exigée qu'en mode strict
74
+ * (`@Idempotent()` par défaut). Cela unifie les deux call-sites :
75
+ * - admin : `required=false` → exige la clé seulement en WS (comportement S4) ;
76
+ * - `@Idempotent()` : `required=true` (strict) → exige la clé même en HTTP ;
77
+ * - `@Idempotent({required:false})` : souple en HTTP, mais toujours strict en WS.
78
+ */
79
+ export declare function evaluateIdempotency(opts: {
80
+ /**
81
+ * Store résolu, ou `null`/`undefined` (service absent → dégrade en exécution
82
+ * directe). Accepte les deux : `Controller.get()` renvoie `null`, un
83
+ * `container.get()` peut renvoyer `undefined`.
84
+ */
85
+ store: IIdempotencyStore | null | undefined;
86
+ /** Identité scope (cf {@link resolveIdentity}), ou `null` → pas de cache. */
87
+ identity: string | null;
88
+ /** Clé client résolue (cf {@link resolveIdempotencyKey}), ou `undefined`. */
89
+ clientKey: string | undefined;
90
+ /** Empreinte du payload (cf {@link computeFingerprint}). */
91
+ fingerprint: string;
92
+ /** La requête arrive-t-elle par socket ? (clé alors obligatoire.) */
93
+ isWs: boolean;
94
+ /** Exigence déclarée d'une clé (mode strict). */
95
+ required: boolean;
96
+ }): Promise<IdempotencyVerdict>;
@@ -0,0 +1,30 @@
1
+ import { GcScheduler, type IIdempotencyStore } from "nodefony";
2
+ /**
3
+ * Options d'armement du balayage périodique d'un store d'idempotence.
4
+ */
5
+ export interface IIdempotencyGcOptions {
6
+ /** Intervalle entre deux purges (s). 0 = désarmé (cron / TTL natif). */
7
+ intervalS: number;
8
+ /** Étale le départ (anti thundering-herd cluster sur le store SQL partagé). */
9
+ jitter: boolean;
10
+ /** Reçoit l'erreur d'une passe de gc qui a levé. */
11
+ onError: (error: unknown) => void;
12
+ /** Journalise l'armement (observabilité boot / preuve e2e). */
13
+ log?: (message: string) => void;
14
+ }
15
+ /**
16
+ * Arme un {@link GcScheduler} qui purge périodiquement les entrées expirées d'un
17
+ * store d'idempotence — **UNIQUEMENT si le store expose `gc()`**.
18
+ *
19
+ * Pourquoi ce gating : un store à **expiration native** (`redis` → `SET … PX`) ou à
20
+ * **purge passive** (`memory` → éviction FIFO au cap) **n'expose pas** `gc()` ; les
21
+ * brancher sur un timer serait un no-op coûteux. Seul un store **SQL** (`drizzle`,
22
+ * `DELETE WHERE expiresAt <= now`) en a besoin — et son `gc()` était jusqu'ici
23
+ * **orphelin** (jamais appelé → fuite : les clés mortes s'accumulaient en base).
24
+ * Ce helper ferme ce trou, et l'isole de `onKernelBoot` pour être **testable sans
25
+ * booter un kernel**.
26
+ *
27
+ * @returns le scheduler armé (à `stop()` au shutdown), ou `null` si le store n'a
28
+ * pas de `gc()` (rien à planifier).
29
+ */
30
+ export declare function scheduleIdempotencyGc(store: IIdempotencyStore, opts: IIdempotencyGcOptions): GcScheduler | null;
@@ -0,0 +1,59 @@
1
+ import type { Module, IIdempotencyStore } from "nodefony";
2
+ import type { FrameworkConfig } from "../config/config.js";
3
+ /**
4
+ * Registre de **fabriques de stores d'idempotence DISTRIBUÉS** — résout le nom
5
+ * configuré (`framework.idempotency.store`) vers une instance d'
6
+ * {@link IIdempotencyStore}, SANS coupler le framework à un backend en dur.
7
+ *
8
+ * Pourquoi : le store d'idempotence est pluggable par contrat (le contrat vit au
9
+ * CORE, `nodefony`). Le DÉFAUT mémoire (`MemoryIdempotencyStore`, per-pod) reste
10
+ * enregistré par `@services` du module framework (zéro config, toujours présent)
11
+ * → il n'est PAS dans ce registre. Ce registre ne porte QUE les **overrides
12
+ * distribués opt-in** (`redis`, `drizzle`) : un `if (name === "redis")` dans le
13
+ * framework trahirait la promesse pluggable, et le framework ne peut de toute
14
+ * façon pas importer `@nodefony/redis` (graphe inverse).
15
+ *
16
+ * Inversion de dépendance : les adapters (`@nodefony/redis`/`@nodefony/drizzle`)
17
+ * exportent une classe PURE (`import type` du contrat core) ; l'**application**
18
+ * câble la fabrique (`registerIdempotencyStore("redis", …)`), car activer un
19
+ * store distribué est une décision de **déploiement** (cluster multi-pod), pas
20
+ * une conséquence du chargement du module. Convention-frère : `tokenStoreRegistry`
21
+ * (`@nodefony/security`), `backplaneRegistry` (`@nodefony/realtime`).
22
+ */
23
+ /**
24
+ * Contexte passé à une fabrique de store : de quoi se construire (résolutions
25
+ * coûteuses en lazy à l'intérieur de l'instance). Le module donne accès au
26
+ * container kernel (`ctx.module.kernel?.container?.get("redis")`).
27
+ */
28
+ export interface IIdempotencyStoreFactoryContext {
29
+ /** Module framework (porte `kernel.container` pour résoudre `redis`, etc.). */
30
+ readonly module: Module;
31
+ /** Config framework validée + gelée. */
32
+ readonly config: FrameworkConfig;
33
+ }
34
+ /** Fabrique d'un store d'idempotence pour un nom donné. */
35
+ export type IdempotencyStoreFactory = (ctx: IIdempotencyStoreFactoryContext) => IIdempotencyStore;
36
+ /**
37
+ * Enregistre (ou remplace) la fabrique d'un store d'idempotence distribué.
38
+ * Appelée par l'application pour les adapters (`redis`/`drizzle`).
39
+ */
40
+ export declare function registerIdempotencyStore(name: string, factory: IdempotencyStoreFactory): void;
41
+ /** Fabrique d'un store par nom, ou `undefined` si inconnu. */
42
+ export declare function getIdempotencyStoreFactory(name: string): IdempotencyStoreFactory | undefined;
43
+ /**
44
+ * Noms de stores distribués enregistrés (résolution `auto` + validation boot).
45
+ * N'inclut PAS `"memory"` : dans `resolveAutoStore`, memory est le **fallback**
46
+ * (per-pod), jamais une préférence à sélectionner — l'y mettre fausserait le choix.
47
+ */
48
+ export declare function listIdempotencyStores(): string[];
49
+ /**
50
+ * Backends d'idempotence UTILISABLES, pour l'AFFICHAGE (écran Studio « Stores »).
51
+ * Inclut le builtin `"memory"` (toujours présent via `@services`, per-pod) EN TÊTE
52
+ * + les stores distribués enregistrés. Distinct de {@link listIdempotencyStores}
53
+ * (distribués seuls, pour la résolution) : côté Studio, le store résolu doit
54
+ * TOUJOURS figurer dans les backends dispo — sinon `resolved: "memory"` apparaît
55
+ * absent de la liste (incohérence). Convention-frère : les autres briques
56
+ * enregistrent leur builtin `memory` dans leur registre, donc `listXStores()`
57
+ * l'inclut déjà ; idempotency pose son memory hors registre → on le rajoute ici.
58
+ */
59
+ export declare function listIdempotencyBackends(): string[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Un groupe de scopes d'**une API** — le préfixe avant `:` (`orders`) et la liste
3
+ * (triée) de ses scopes déclarés (`orders:read`, `orders:write`). Un scope sans
4
+ * `:` (ex. `@RequireScope("orders")` = toute l'API) se groupe sous lui-même.
5
+ */
6
+ export interface IApiScopeGroup {
7
+ /** Préfixe d'API (segment avant le `:`). */
8
+ readonly api: string;
9
+ /** Scopes `api:action` déclarés pour cette API, triés et dédupliqués. */
10
+ readonly scopes: readonly string[];
11
+ }
12
+ /**
13
+ * P6.8 — **Découverte au boot** : scanne TOUTES les routes montées (`Router.routes`)
14
+ * et agrège les scopes déclarés par `@RequireScope`, **regroupés par API** (préfixe
15
+ * avant `:`). C'est la source du formulaire « créer une clé API » de Studio : les
16
+ * scopes proposés DÉRIVENT du code (les routes), au lieu d'une liste plate de config
17
+ * qui se périme dès qu'on ajoute un `@RequireScope` sans penser à la config.
18
+ *
19
+ * **Cold path** : appelé à la demande (ouverture du formulaire), jamais sur le hot
20
+ * path requête. Lecture `Reflect` par route → coût proportionnel au nombre de routes,
21
+ * payé une fois par consultation.
22
+ *
23
+ * @returns les groupes triés par nom d'API (chaque groupe a ses scopes triés).
24
+ */
25
+ export declare function collectDeclaredApiScopes(): IApiScopeGroup[];
26
+ export default collectDeclaredApiScopes;
@@ -0,0 +1,52 @@
1
+ import type { FlowStepId } from "nodefony";
2
+ /**
3
+ * Ce que le journal sait TRIER — un seul axe, le temps.
4
+ *
5
+ * Le nom est public (celui de la ligne rendue, `ILogRecord.timeStamp`) ; l'axe
6
+ * technique du driver est l'`uid` du Pdu, un compteur monotone d'émission. Les
7
+ * deux disent la même chose, à ceci près que l'`uid` départage deux logs de la
8
+ * MÊME milliseconde — ce qu'un tri sur l'horodatage seul ne saurait pas faire.
9
+ */
10
+ export declare const SYSLOG_SORTABLE: readonly ["timeStamp"];
11
+ /**
12
+ * Le vocabulaire de filtre du journal — une **donnée**, publiée telle quelle
13
+ * dans le catalogue admin.
14
+ *
15
+ * Il remplace une lecture à la main qui acceptait tout et ne validait rien :
16
+ * `?severity=CRITICAL` (au lieu de `CRITIC`), `?protocol=grpc`, `?flow=nimporte`
17
+ * et même `?severty=ERROR` posaient un critère vide et rendaient le journal
18
+ * ENTIER sous un `200` — la réponse qu'un exploitant lit comme « aucune erreur ».
19
+ *
20
+ * `severity` et `flow` sont **répétables** (`?severity=ERROR&severity=CRITIC`) :
21
+ * c'est le OU dont le viewer a besoin, et la seule raison d'être de la nature
22
+ * `{ each }` du contrat.
23
+ */
24
+ export declare const SYSLOG_FILTERS: {
25
+ /** Corrélation log↔requête (ALS) — match exact, la clé de la trace. */
26
+ readonly requestId: "string";
27
+ /** Nom de module/service — inclusion insensible à la casse côté driver. */
28
+ readonly module: "string";
29
+ /** Catégorie de message (msgid) — inclusion insensible à la casse. */
30
+ readonly msgid: "string";
31
+ /** Protocole d'origine ; absent = les deux. */
32
+ readonly protocol: readonly ["ws", "http"];
33
+ /**
34
+ * Sévérités RFC 5424 — l'allowlist EST {@link SEVERITY_NAMES} (source unique
35
+ * du cœur), jamais une liste recopiée qui finirait par diverger de l'enum.
36
+ */
37
+ readonly severity: {
38
+ readonly each: readonly ["EMERGENCY", "ALERT", "CRITIC", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"];
39
+ };
40
+ /**
41
+ * Étapes du cycle de vie — l'allowlist est dérivée de la table `FLOW_STEPS`
42
+ * elle-même : ajouter une étape suffit à la rendre filtrable, et aucune
43
+ * seconde liste ne peut se périmer.
44
+ */
45
+ readonly flow: {
46
+ readonly each: FlowStepId[];
47
+ };
48
+ /** Borne basse d'horodatage (epoch ms, incluse). */
49
+ readonly from: "int";
50
+ /** Borne haute d'horodatage (epoch ms, incluse). */
51
+ readonly to: "int";
52
+ };