@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,137 @@
1
+ import { RequestContext } from "nodefony";
2
+ import { createHash } from "node:crypto";
3
+ //#region nodefony/src/idempotency.ts
4
+ /**
5
+ * Logique **pure et partagée** de la porte d'idempotence des mutations, conforme à
6
+ * `draft-ietf-httpapi-idempotency-key-header-06`. Source unique de la sémantique
7
+ * (statuts normatifs, scope de clé, fingerprint, bornage) consommée par les DEUX
8
+ * call-sites qui mutent :
9
+ * - le **data plane admin** (`AdminApiController` — réponse `{status,headers,body}`) ;
10
+ * - les **controllers userland** décorés `@Idempotent` (seam `Resolver`).
11
+ *
12
+ * Le helper ne connaît AUCUN transport ni format de réponse : il rend un
13
+ * {@link IdempotencyVerdict} neutre que chaque call-site traduit dans son monde
14
+ * (court-circuit HTTP, `RpcError` WS, throw `nodefonyError`…). Garantit que la
15
+ * sémantique IETF est décidée à UN seul endroit (cf retex S4 : statuts décidés
16
+ * depuis la spec, pas de mémoire).
17
+ */
18
+ /**
19
+ * Méthodes HTTP considérées comme **mutations** (non sûres, RFC 9110 §9.2.1) →
20
+ * éligibles à l'idempotence. Les méthodes sûres (GET/HEAD/OPTIONS/TRACE) et le
21
+ * pseudo-verbe `WEBSOCKET` n'en font pas partie → `@Idempotent` y est un no-op.
22
+ */
23
+ const MUTATION_METHODS = /* @__PURE__ */ new Set([
24
+ "POST",
25
+ "PUT",
26
+ "PATCH",
27
+ "DELETE"
28
+ ]);
29
+ /** `true` si la méthode est une mutation éligible à l'idempotence. */
30
+ function isMutationMethod(method) {
31
+ return method != null && MUTATION_METHODS.has(method.toUpperCase());
32
+ }
33
+ /**
34
+ * Borne d'une clé d'idempotence (convention Stripe). Une clé est un identifiant
35
+ * court (UUID) ; au-delà, elle est traitée comme ABSENTE (anti-DoS du cache borné).
36
+ */
37
+ const IDEMPOTENCY_KEY_MAX = 255;
38
+ /**
39
+ * Résout la clé d'idempotence d'une requête : posée dans l'ALS par le pont WS
40
+ * (`als.idempotencyKey`) ou lue de l'en-tête HTTP `Idempotency-Key` (clé
41
+ * minuscule côté Node, éventuellement répétée → premier élément). L'ALS prime sur
42
+ * l'en-tête. Une clé > {@link IDEMPOTENCY_KEY_MAX} est traitée comme **absente**
43
+ * (anti-DoS) plutôt que stockée. `undefined` si rien d'exploitable.
44
+ */
45
+ function resolveIdempotencyKey(alsKey, header) {
46
+ let raw;
47
+ if (typeof alsKey === "string" && alsKey) raw = alsKey;
48
+ else if (typeof header === "string" && header) raw = header;
49
+ else if (Array.isArray(header) && typeof header[0] === "string" && header[0]) raw = header[0];
50
+ return raw && raw.length <= 255 ? raw : void 0;
51
+ }
52
+ /**
53
+ * Identité stable pour **scoper** le cache d'idempotence (anti-IDOR : un
54
+ * utilisateur ne doit jamais rejouer la clé d'un autre). Dérivée de l'utilisateur
55
+ * (`username` / `identifier` / `id`, sans coupler le framework au contrat `IUser`),
56
+ * avec fallback sur l'`userId` de l'ALS. `null` = pas d'identité fiable → l'appelant
57
+ * n'utilise PAS le cache (jamais de partage cross-identité).
58
+ *
59
+ * ⚠️ Doit renvoyer la MÊME valeur quel que soit le transport pour un même compte
60
+ * (sinon une mutation tentée en WS puis rejouée en HTTP ne dédoublonnerait pas).
61
+ * D'où la dérivation de `request.user` (posé UNIFORMÉMENT dans l'ALS par les deux
62
+ * transports), et NON de `getUserId()` seul (le firewall HTTP ne le pose pas
63
+ * toujours — vécu S4).
64
+ */
65
+ function resolveIdentity(user) {
66
+ if (user && typeof user === "object") {
67
+ const o = user;
68
+ for (const v of [
69
+ o.username,
70
+ o.identifier,
71
+ o.id
72
+ ]) if (typeof v === "string" && v) return v;
73
+ }
74
+ const uid = RequestContext.getUserId();
75
+ return typeof uid === "string" && uid ? uid : null;
76
+ }
77
+ /**
78
+ * Empreinte SHA-256 du **payload** d'une requête (parties sérialisables : nom de
79
+ * route + params + corps). Comparée par le store à l'empreinte mémorisée pour une
80
+ * clé : si elle diffère, la clé est réutilisée pour une AUTRE requête → 422
81
+ * (draft §2.4). Hash → empreinte courte (anti-DoS mémoire) + comparaison O(1).
82
+ */
83
+ function computeFingerprint(parts) {
84
+ return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
85
+ }
86
+ /**
87
+ * Cœur normatif : à partir de la clé client, de l'identité, du fingerprint et du
88
+ * transport, rend le {@link IdempotencyVerdict} à appliquer. La réservation
89
+ * (`store.begin`) est **atomique** (mono-thread JS côté mémoire, `SET … NX` côté
90
+ * Redis) ; le store peut être sync (mémoire) ou async (distribué) → `begin` est
91
+ * `await`é, d'où le retour `Promise<IdempotencyVerdict>`.
92
+ *
93
+ * Le `required` **effectif** = `required || isWs` : une mutation par socket exige
94
+ * TOUJOURS une clé (le WS reconnecte/rejoue → muter sans garde-fou exposerait au
95
+ * double-effet), tandis qu'en HTTP la clé n'est exigée qu'en mode strict
96
+ * (`@Idempotent()` par défaut). Cela unifie les deux call-sites :
97
+ * - admin : `required=false` → exige la clé seulement en WS (comportement S4) ;
98
+ * - `@Idempotent()` : `required=true` (strict) → exige la clé même en HTTP ;
99
+ * - `@Idempotent({required:false})` : souple en HTTP, mais toujours strict en WS.
100
+ */
101
+ async function evaluateIdempotency(opts) {
102
+ const requiredEffective = opts.required || opts.isWs;
103
+ if (!opts.clientKey) {
104
+ if (requiredEffective) return {
105
+ kind: "reject",
106
+ status: 400,
107
+ message: opts.isWs ? "Idempotency-Key required for socket mutations" : "Idempotency-Key required"
108
+ };
109
+ return { kind: "execute" };
110
+ }
111
+ if (!opts.store || !opts.identity) return { kind: "execute" };
112
+ const key = JSON.stringify([opts.identity, opts.clientKey]);
113
+ const outcome = await opts.store.begin(key, opts.fingerprint);
114
+ switch (outcome.state) {
115
+ case "mismatch": return {
116
+ kind: "reject",
117
+ status: 422,
118
+ message: "Idempotency-Key is already used",
119
+ detail: "This Idempotency-Key was used with a different payload; a key must not be reused across different requests."
120
+ };
121
+ case "in-flight": return {
122
+ kind: "reject",
123
+ status: 409,
124
+ message: "Conflict: an identical request is already in progress"
125
+ };
126
+ case "replayed": return {
127
+ kind: "replay",
128
+ response: outcome.response
129
+ };
130
+ default: return {
131
+ kind: "guarded",
132
+ key
133
+ };
134
+ }
135
+ }
136
+ //#endregion
137
+ export { IDEMPOTENCY_KEY_MAX, computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity };
@@ -0,0 +1,32 @@
1
+ import { GcScheduler } from "nodefony";
2
+ //#region nodefony/src/idempotencyGc.ts
3
+ /**
4
+ * Arme un {@link GcScheduler} qui purge périodiquement les entrées expirées d'un
5
+ * store d'idempotence — **UNIQUEMENT si le store expose `gc()`**.
6
+ *
7
+ * Pourquoi ce gating : un store à **expiration native** (`redis` → `SET … PX`) ou à
8
+ * **purge passive** (`memory` → éviction FIFO au cap) **n'expose pas** `gc()` ; les
9
+ * brancher sur un timer serait un no-op coûteux. Seul un store **SQL** (`drizzle`,
10
+ * `DELETE WHERE expiresAt <= now`) en a besoin — et son `gc()` était jusqu'ici
11
+ * **orphelin** (jamais appelé → fuite : les clés mortes s'accumulaient en base).
12
+ * Ce helper ferme ce trou, et l'isole de `onKernelBoot` pour être **testable sans
13
+ * booter un kernel**.
14
+ *
15
+ * @returns le scheduler armé (à `stop()` au shutdown), ou `null` si le store n'a
16
+ * pas de `gc()` (rien à planifier).
17
+ */
18
+ function scheduleIdempotencyGc(store, opts) {
19
+ if (typeof store.gc !== "function") return null;
20
+ const runGc = store.gc.bind(store);
21
+ const scheduler = new GcScheduler({
22
+ intervalS: opts.intervalS,
23
+ jitter: opts.jitter,
24
+ run: () => runGc(),
25
+ onError: opts.onError
26
+ });
27
+ const armed = scheduler.start();
28
+ opts.log?.(armed ? `idempotency gc armed — purge every ${opts.intervalS}s (SQL store exposes gc())` : `idempotency gc disarmed — intervalS=${opts.intervalS} (delegated to cron / external)`);
29
+ return scheduler;
30
+ }
31
+ //#endregion
32
+ export { scheduleIdempotencyGc };
@@ -0,0 +1,36 @@
1
+ //#region nodefony/src/idempotencyStoreRegistry.ts
2
+ const factories = /* @__PURE__ */ new Map();
3
+ /**
4
+ * Enregistre (ou remplace) la fabrique d'un store d'idempotence distribué.
5
+ * Appelée par l'application pour les adapters (`redis`/`drizzle`).
6
+ */
7
+ function registerIdempotencyStore(name, factory) {
8
+ factories.set(name, factory);
9
+ }
10
+ /** Fabrique d'un store par nom, ou `undefined` si inconnu. */
11
+ function getIdempotencyStoreFactory(name) {
12
+ return factories.get(name);
13
+ }
14
+ /**
15
+ * Noms de stores distribués enregistrés (résolution `auto` + validation boot).
16
+ * N'inclut PAS `"memory"` : dans `resolveAutoStore`, memory est le **fallback**
17
+ * (per-pod), jamais une préférence à sélectionner — l'y mettre fausserait le choix.
18
+ */
19
+ function listIdempotencyStores() {
20
+ return [...factories.keys()];
21
+ }
22
+ /**
23
+ * Backends d'idempotence UTILISABLES, pour l'AFFICHAGE (écran Studio « Stores »).
24
+ * Inclut le builtin `"memory"` (toujours présent via `@services`, per-pod) EN TÊTE
25
+ * + les stores distribués enregistrés. Distinct de {@link listIdempotencyStores}
26
+ * (distribués seuls, pour la résolution) : côté Studio, le store résolu doit
27
+ * TOUJOURS figurer dans les backends dispo — sinon `resolved: "memory"` apparaît
28
+ * absent de la liste (incohérence). Convention-frère : les autres briques
29
+ * enregistrent leur builtin `memory` dans leur registre, donc `listXStores()`
30
+ * l'inclut déjà ; idempotency pose son memory hors registre → on le rajoute ici.
31
+ */
32
+ function listIdempotencyBackends() {
33
+ return ["memory", ...factories.keys()];
34
+ }
35
+ //#endregion
36
+ export { getIdempotencyStoreFactory, listIdempotencyBackends, listIdempotencyStores, registerIdempotencyStore };
@@ -0,0 +1,40 @@
1
+ import { extractActionScopes } from "../decorators/routerDecorators.js";
2
+ import router_default from "../service/router.js";
3
+ //#region nodefony/src/scopeCatalog.ts
4
+ /**
5
+ * P6.8 — **Découverte au boot** : scanne TOUTES les routes montées (`Router.routes`)
6
+ * et agrège les scopes déclarés par `@RequireScope`, **regroupés par API** (préfixe
7
+ * avant `:`). C'est la source du formulaire « créer une clé API » de Studio : les
8
+ * scopes proposés DÉRIVENT du code (les routes), au lieu d'une liste plate de config
9
+ * qui se périme dès qu'on ajoute un `@RequireScope` sans penser à la config.
10
+ *
11
+ * **Cold path** : appelé à la demande (ouverture du formulaire), jamais sur le hot
12
+ * path requête. Lecture `Reflect` par route → coût proportionnel au nombre de routes,
13
+ * payé une fois par consultation.
14
+ *
15
+ * @returns les groupes triés par nom d'API (chaque groupe a ses scopes triés).
16
+ */
17
+ function collectDeclaredApiScopes() {
18
+ const byApi = /* @__PURE__ */ new Map();
19
+ for (const route of router_default.routes) {
20
+ const ctor = route.controller;
21
+ const method = route.classMethod;
22
+ if (!ctor || !method) continue;
23
+ for (const scope of extractActionScopes(ctor, method)) {
24
+ const i = scope.indexOf(":");
25
+ const api = i === -1 ? scope : scope.slice(0, i);
26
+ let set = byApi.get(api);
27
+ if (set === void 0) {
28
+ set = /* @__PURE__ */ new Set();
29
+ byApi.set(api, set);
30
+ }
31
+ set.add(scope);
32
+ }
33
+ }
34
+ return [...byApi.keys()].sort().map((api) => ({
35
+ api,
36
+ scopes: [...byApi.get(api)].sort()
37
+ }));
38
+ }
39
+ //#endregion
40
+ export { collectDeclaredApiScopes, collectDeclaredApiScopes as default };
@@ -0,0 +1,51 @@
1
+ import { FLOW_STEPS, SEVERITY_NAMES } from "nodefony";
2
+ //#region nodefony/src/syslogFilters.ts
3
+ /**
4
+ * Ce que le journal sait TRIER — un seul axe, le temps.
5
+ *
6
+ * Le nom est public (celui de la ligne rendue, `ILogRecord.timeStamp`) ; l'axe
7
+ * technique du driver est l'`uid` du Pdu, un compteur monotone d'émission. Les
8
+ * deux disent la même chose, à ceci près que l'`uid` départage deux logs de la
9
+ * MÊME milliseconde — ce qu'un tri sur l'horodatage seul ne saurait pas faire.
10
+ */
11
+ const SYSLOG_SORTABLE = ["timeStamp"];
12
+ /**
13
+ * Le vocabulaire de filtre du journal — une **donnée**, publiée telle quelle
14
+ * dans le catalogue admin.
15
+ *
16
+ * Il remplace une lecture à la main qui acceptait tout et ne validait rien :
17
+ * `?severity=CRITICAL` (au lieu de `CRITIC`), `?protocol=grpc`, `?flow=nimporte`
18
+ * et même `?severty=ERROR` posaient un critère vide et rendaient le journal
19
+ * ENTIER sous un `200` — la réponse qu'un exploitant lit comme « aucune erreur ».
20
+ *
21
+ * `severity` et `flow` sont **répétables** (`?severity=ERROR&severity=CRITIC`) :
22
+ * c'est le OU dont le viewer a besoin, et la seule raison d'être de la nature
23
+ * `{ each }` du contrat.
24
+ */
25
+ const SYSLOG_FILTERS = {
26
+ /** Corrélation log↔requête (ALS) — match exact, la clé de la trace. */
27
+ requestId: "string",
28
+ /** Nom de module/service — inclusion insensible à la casse côté driver. */
29
+ module: "string",
30
+ /** Catégorie de message (msgid) — inclusion insensible à la casse. */
31
+ msgid: "string",
32
+ /** Protocole d'origine ; absent = les deux. */
33
+ protocol: ["ws", "http"],
34
+ /**
35
+ * Sévérités RFC 5424 — l'allowlist EST {@link SEVERITY_NAMES} (source unique
36
+ * du cœur), jamais une liste recopiée qui finirait par diverger de l'enum.
37
+ */
38
+ severity: { each: SEVERITY_NAMES },
39
+ /**
40
+ * Étapes du cycle de vie — l'allowlist est dérivée de la table `FLOW_STEPS`
41
+ * elle-même : ajouter une étape suffit à la rendre filtrable, et aucune
42
+ * seconde liste ne peut se périmer.
43
+ */
44
+ flow: { each: Object.keys(FLOW_STEPS) },
45
+ /** Borne basse d'horodatage (epoch ms, incluse). */
46
+ from: "int",
47
+ /** Borne haute d'horodatage (epoch ms, incluse). */
48
+ to: "int"
49
+ };
50
+ //#endregion
51
+ export { SYSLOG_FILTERS, SYSLOG_SORTABLE };
@@ -0,0 +1,96 @@
1
+ import { Kernel, Module } from "nodefony";
2
+ import type { FrameworkConfigInput, FrameworkConfig } from "./nodefony/config/config.js";
3
+ import { getIdempotencyStoreFactory, registerIdempotencyStore, listIdempotencyStores } from "./nodefony/src/idempotencyStoreRegistry.js";
4
+ import Router from "./nodefony/service/router.js";
5
+ import Route from "./nodefony/src/Route.js";
6
+ import Controller from "./nodefony/src/Controller.js";
7
+ import ResourceController from "./nodefony/src/ResourceController.js";
8
+ import Resolver from "./nodefony/src/Resolver.js";
9
+ import AdminBroker from "./nodefony/service/AdminBroker.js";
10
+ import MemoryIdempotencyStore from "./nodefony/service/IdempotencyStore.js";
11
+ import AdminApiController from "./nodefony/controller/AdminApiController.js";
12
+ import SessionAuthController, { mountSessionAuthRoutes } from "./nodefony/controller/SessionAuthController.js";
13
+ import TokenAuthController, { mountTokenAuthRoutes } from "./nodefony/controller/TokenAuthController.js";
14
+ import IssuerMetadataController, { mountIssuerMetadataRoutes } from "./nodefony/controller/IssuerMetadataController.js";
15
+ import ProtectedResourceMetadataController, { mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources } from "./nodefony/controller/ProtectedResourceMetadataController.js";
16
+ import WebAuthnController, { mountWebAuthnRoutes } from "./nodefony/controller/WebAuthnController.js";
17
+ import OAuth2Controller, { mountOAuth2Routes } from "./nodefony/controller/OAuth2Controller.js";
18
+ import BenchController, { mountBenchRoutes } from "./nodefony/controller/BenchController.js";
19
+ import ApiKeyController, { mountApiKeyRoutes } from "./nodefony/controller/ApiKeyController.js";
20
+ import TotpController, { mountTotpRoutes } from "./nodefony/controller/TotpController.js";
21
+ import { createKernelAdminApi } from "./nodefony/src/KernelAdminApi.js";
22
+ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
23
+ import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
24
+ import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
25
+ import Eta from "./nodefony/service/Eta.js";
26
+ import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
27
+ import { mergeSchemas, makeExecutableSchema } from "@graphql-tools/schema";
28
+ import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
29
+ declare module "nodefony" {
30
+ interface NodefonyModuleConfig {
31
+ "@nodefony/framework": FrameworkConfigInput;
32
+ }
33
+ }
34
+ declare class Framework extends Module<FrameworkConfig> {
35
+ #private;
36
+ constructor(kernel: Kernel);
37
+ /** JSON Schema de la config framework → data plane admin (config riche Studio). */
38
+ configSchema(): unknown;
39
+ /**
40
+ * Phase `onRegister` : valide la config du module via le builder
41
+ * ({@link defineFrameworkConfig}) AVANT que les `@services` (Router,
42
+ * AdminBroker — qui lisent `module.options.router`/`.adminBroker`) ne soient
43
+ * instanciés à `onBoot`. Une config invalide plante proprement ici avec un
44
+ * message clair, plutôt qu'un `undefined.x` silencieux en runtime
45
+ * (cf `feedback_config_validation_zod`). La config validée est ré-assignée à
46
+ * `this.options` (matérialise les défauts, préserve `router`/`adminBroker`).
47
+ */
48
+ onKernelRegister(): Promise<this>;
49
+ /**
50
+ * Phase `onBoot` (après les `@services` à `onPreBoot` → le défaut mémoire
51
+ * `idempotencyStore` est déjà enregistré, ET après l'`onPreBoot` des autres
52
+ * modules → le service `redis` est résoluble) : si la config sélectionne un
53
+ * store d'idempotence **distribué** (`idempotency.store` ≠ `memory`), le résout
54
+ * via le registre et **override** le service `idempotencyStore` → toutes les
55
+ * mutations (`@Idempotent` + data plane admin) dédupliquent cross-pod.
56
+ *
57
+ * **Politique en cas d'échec de résolution** (nom inconnu, ou store distribué
58
+ * qui ne peut pas s'initialiser — ex. `idempotency.store="redis"` sans module
59
+ * `@nodefony/redis`) :
60
+ * - **prod** → **fatal** (rethrow → le module framework est `critical` → boot
61
+ * avorté) : en cluster multi-pod, dégrader en silence vers le cache per-pod
62
+ * serait du double-effet non dédupliqué (cf « pas de dégradation silencieuse »).
63
+ * - **dev/test** (mono-pod) → **WARNING fort + fallback sur le cache mémoire**
64
+ * déjà en place : la dédup per-pod suffit hors cluster, et on ne casse PAS le
65
+ * framework (= le routeur) pour une option d'infra absente en local. Jamais
66
+ * silencieux (le WARNING annonce la dégradation).
67
+ */
68
+ onKernelBoot(): Promise<this>;
69
+ /**
70
+ * Phase `onReady` (après le boot de tous les modules) : enregistre le
71
+ * producteur admin du kernel puis monte le data plane `/nodefony/<ns>/api/*`.
72
+ *
73
+ * Les autres modules s'enregistrent dans leur `onKernelBoot` / `onKernelReady`
74
+ * (le module realtime auto-enregistre son producteur `realtime` ici) → tous
75
+ * présents au moment du `mountAll()` qui clôt cette phase.
76
+ */
77
+ onKernelReady(): Promise<this>;
78
+ }
79
+ declare const graphql: {
80
+ mergeSchemas: typeof mergeSchemas;
81
+ makeExecutableSchema: typeof makeExecutableSchema;
82
+ mergeResolvers: typeof mergeResolvers;
83
+ mergeTypeDefs: typeof mergeTypeDefs;
84
+ };
85
+ export default Framework;
86
+ export { Controller, ResourceController, Route, Router, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, graphql, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
87
+ export { frameworkConfigSchema } from "./nodefony/config/config.js";
88
+ export type { IdempotencyStoreFactory, IIdempotencyStoreFactoryContext, } from "./nodefony/src/idempotencyStoreRegistry.js";
89
+ export type { SecurityClause, SecurityRequirement, CspDirectives, } from "./nodefony/decorators/routerDecorators.js";
90
+ export type { ControllerScope } from "./nodefony/src/Controller.js";
91
+ export type { FrameworkAdminApiOptions } from "./nodefony/src/FrameworkAdminApi.js";
92
+ export type { PlaygroundAction, PlaygroundController, PlaygroundGuards, PlaygroundParam, } from "./nodefony/src/PlaygroundAdminApi.js";
93
+ export type { IResourceService, IResourceReadOptions, IResourcePageQuery, } from "./nodefony/src/ResourceController.js";
94
+ export type { FrameworkConfig, FrameworkConfigInput, } from "./nodefony/config/config.js";
95
+ export { defineFrameworkConfig, frameworkConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
96
+ export type { IController, IRoute, IResolver, IAdminBroker, IAdminRoute, IIdempotencyStore, IdempotencyOutcome, IdempotentResponse, } from "./nodefony/interfaces/index.js";
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export declare const frameworkConfigSchema: z.ZodObject<{
3
+ router: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
4
+ adminBroker: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
5
+ idempotency: z.ZodDefault<z.ZodObject<{
6
+ store: z.ZodDefault<z.ZodString>;
7
+ gcIntervalS: z.ZodDefault<z.ZodNumber>;
8
+ gcJitter: z.ZodDefault<z.ZodBoolean>;
9
+ }, z.core.$strict>>;
10
+ }, z.core.$strict>;
11
+ /** Type de sortie (config normalisée + défauts appliqués). */
12
+ export type FrameworkConfig = z.infer<typeof frameworkConfigSchema>;
13
+ /** Type d'entrée (toutes sections omissibles — défauts du schéma). */
14
+ export type FrameworkConfigInput = z.input<typeof frameworkConfigSchema>;
15
+ declare const _default: {
16
+ router?: {
17
+ [x: string]: unknown;
18
+ } | undefined;
19
+ adminBroker?: {
20
+ [x: string]: unknown;
21
+ } | undefined;
22
+ idempotency: {
23
+ store: string;
24
+ gcIntervalS: number;
25
+ gcJitter: boolean;
26
+ };
27
+ "module-security": {
28
+ areas: {
29
+ "nodefony-liveness": {
30
+ pattern: string;
31
+ authenticators: string[];
32
+ realtime: boolean;
33
+ };
34
+ "nodefony-admin": {
35
+ pattern: string;
36
+ authenticators: string[];
37
+ };
38
+ };
39
+ };
40
+ };
41
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import type { FrameworkConfig, FrameworkConfigInput } from "./config.js";
2
+ /**
3
+ * Builder type-safe de la configuration de `@nodefony/framework` (PUR — ne
4
+ * retape JAMAIS un défaut : source unique = `./config.ts`).
5
+ *
6
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
7
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
8
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
9
+ * env + freeze) et publie le JSON Schema Studio.
10
+ *
11
+ * Valide la config brute contre le schéma Zod et matérialise les défauts.
12
+ * Une config invalide échoue avec un message lisible par champ (fail-loud au
13
+ * boot, plutôt qu'un `undefined.x` silencieux en runtime).
14
+ *
15
+ * @param config - configuration brute (sections omises = défauts sûrs).
16
+ * @returns config validée (défauts matérialisés, `router`/`adminBroker`
17
+ * préservés tels quels — bags loose non strippés).
18
+ * @throws BootConfigurationError si la config est invalide ou porte une clé
19
+ * inconnue — le boot s'interrompt, en dev comme en prod.
20
+ */
21
+ export declare function defineFrameworkConfig(config?: FrameworkConfigInput): FrameworkConfig;
22
+ /**
23
+ * JSON Schema introspectable de la config framework — destiné au formulaire
24
+ * d'édition Studio et à la documentation générée (les flags de champ posés via
25
+ * `.meta()` sont recopiés par `z.toJSONSchema`).
26
+ */
27
+ export declare function frameworkConfigJsonSchema(): unknown;
@@ -0,0 +1,70 @@
1
+ import type { ContextType } from "@nodefony/http";
2
+ import Controller from "../src/Controller.js";
3
+ /**
4
+ * Controller pont unique du data plane admin (Studio).
5
+ *
6
+ * Toutes les routes `/nodefony/<namespace>/api/*` montées par le broker
7
+ * pointent vers `AdminApiController.dispatch`. À l'exécution, le controller :
8
+ * 1. retrouve l'`IAdminRoute` via le nom de route (lookup O(1) du broker) ;
9
+ * 2. projette le `Context` HTTP en {@link IAdminRequest} (découplage core) ;
10
+ * 3. applique le RBAC (différé tant que P6/auth n'est pas câblé) ;
11
+ * 4. appelle le handler du producteur et sérialise le retour en JSON.
12
+ *
13
+ * Un seul controller réutilisé pour N endpoints : pas de génération dynamique
14
+ * de classes, et chaque route reste une vraie `Route` (404/405 du Router OK).
15
+ */
16
+ declare class AdminApiController extends Controller {
17
+ /**
18
+ * Identité de l'instance qui répond — `NF_INSTANCE_ID` (k8s pod, worker)
19
+ * ou `pid` en fallback. Même convention que les providers realtime Studio.
20
+ * Calculée une fois (statique) : invariante sur la vie du process.
21
+ */
22
+ static readonly instanceId: string;
23
+ constructor(context: ContextType);
24
+ /**
25
+ * Action générique appelée par le Resolver pour toute route admin — DUPLEX
26
+ * (« API souveraine ») : même exécution sur les deux transports, seul
27
+ * l'emballage diffère.
28
+ *
29
+ * - **HTTP** : rendu historique inchangé (`renderJson` + status + header
30
+ * `x-nodefony-instance`).
31
+ * - **Pont WS-RPC `api.request`** : valeur **nue** (le pont l'enveloppe
32
+ * `{id, result}` — snapshot ≡ GET par construction) ; statut ≥ 400 →
33
+ * {@link RpcError} (`data.status` + `data.body`), symétrie d'un `fetch`
34
+ * qui expose son statut. L'identité d'instance n'est pas répétée par
35
+ * réponse : une socket est tenue par UN process (info de connexion).
36
+ *
37
+ * @param args - variables de route positionnelles (`{id}`…), zippées avec
38
+ * `route.variables` pour reconstruire `request.params`.
39
+ */
40
+ dispatch(...args: unknown[]): Promise<unknown>;
41
+ /**
42
+ * Résout la route admin, puis délègue l'exécution à la porte unique du cœur.
43
+ *
44
+ * Le LOOKUP est ce qui reste propre à ce transport : ici par **nom de route**
45
+ * (le Router l'impose), là où la CLI et le serveur MCP résolvent par couple
46
+ * namespace/chemin. Tout ce qui suit — autorisation, idempotence, handler,
47
+ * normalisation, traduction des erreurs — est commun, donc partagé
48
+ * ({@link executeAdminEndpoint}) : deux implémentations divergeraient, et
49
+ * c'est la porte la moins relue qui deviendrait la plus permissive.
50
+ */
51
+ private runAdmin;
52
+ /**
53
+ * Porte d'idempotence d'une **mutation** admin. La sémantique normative
54
+ * (`draft-ietf-httpapi-idempotency-key-header` : 400 clé requise WS / 409
55
+ * concurrent / 422 mismatch / rejeu mémorisé, clé scopée identité anti-IDOR,
56
+ * fingerprint du payload) vit dans le **helper partagé** `idempotency.ts` — le
57
+ * MÊME que le seam `Resolver` des controllers userland `@Idempotent`. Ici on ne
58
+ * fait que TRADUIRE le verdict neutre en forme admin (`shortCircuit` immédiat,
59
+ * ou callbacks `onSuccess`/`onFailure` autour de l'exécution).
60
+ *
61
+ * `required: false` → l'admin n'exige la clé qu'en WS (porté par `isWs` dans le
62
+ * helper) ; en HTTP, une mutation sans clé s'exécute directement (historique).
63
+ */
64
+ private idempotencyGate;
65
+ /** Projette le Context courant en requête admin normalisée. */
66
+ private buildRequest;
67
+ /** Extrait les rôles de l'utilisateur ALS sans coupler le core à `IUser`. */
68
+ private extractRoles;
69
+ }
70
+ export default AdminApiController;
@@ -0,0 +1,49 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Endpoints HTTP de **gestion des clés API personnelles (PAT, P6.12)** — console
6
+ * « mes clés » façon GitHub, adaptateurs MINCES au-dessus du service `apiKeys`
7
+ * (`@nodefony/security`) :
8
+ *
9
+ * - `POST /nodefony/security/api/keys` — body `{name, scopes?, expiresInDays?}`
10
+ * → `201 {id, prefix, name, scopes, token, …}` (le `token` clair n'apparaît qu'ICI)
11
+ * - `GET /nodefony/security/api/keys` — liste les clés du porteur (sans secret)
12
+ * - `DELETE /nodefony/security/api/keys/{id}` — révoque → `200 {ok:true}` / `404`
13
+ * si la clé n'existe pas **ou** appartient à autrui (indiscernable, jamais 403)
14
+ *
15
+ * **PAS de `bypassFirewall`** (≠ login/token/oauth) : ces routes vivent DANS la
16
+ * zone data plane `^/nodefony/[^/]+/api(/|$)` → **session BFF requise**. Le porteur
17
+ * est TOUJOURS l'utilisateur courant (`authFlow.me`), jamais un paramètre — on ne
18
+ * crée/révoque jamais une clé pour autrui. Montés seulement si le service `apiKeys`
19
+ * existe (security chargé + clés activées) → 404, zéro surface, sinon.
20
+ *
21
+ * Erreurs mappées par DUCK-TYPING sur `code` (400/409/503) — framework ne peut pas
22
+ * importer les classes d'erreur de security.
23
+ */
24
+ declare class ApiKeyController extends Controller {
25
+ #private;
26
+ constructor(context: ContextType);
27
+ /** Émission : crée une clé pour le porteur courant → token clair (1×). */
28
+ create(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
29
+ /**
30
+ * Capacités/contraintes d'émission (plafond, scopes, préfixe, durée par défaut)
31
+ * — alimente le formulaire de création. Lecture pure, aucune valeur sensible ;
32
+ * accessible à tout porteur authentifié (zone data plane, session BFF).
33
+ */
34
+ capabilities(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
35
+ /** Liste les clés du porteur courant (vue publique, sans secret). */
36
+ list(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
37
+ /** Révocation : seulement une clé DU porteur courant (sinon 404, anti-énumération). */
38
+ revoke(id: unknown): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
39
+ }
40
+ /**
41
+ * Monte les routes de gestion des clés API — appelé par le module framework à
42
+ * `onKernelReady`, seulement si le service `apiKeys` est présent.
43
+ *
44
+ * Routes nommées `security.apikeys.*` (espace data plane `/nodefony/security/api/*`).
45
+ * **Aucun `bypassFirewall`** : l'aire data plane (session BFF) les garde — c'est
46
+ * voulu (gérer ses clés exige d'être authentifié).
47
+ */
48
+ export declare function mountApiKeyRoutes(frameworkModule: Module): void;
49
+ export default ApiKeyController;
@@ -0,0 +1,45 @@
1
+ import type { Module } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Cible de mesure du **pipeline applicatif** — un controller ordinaire qui rend un
6
+ * corps figé, et rien d'autre.
7
+ *
8
+ * **Pourquoi un controller et pas un endpoint du data plane admin** : une route
9
+ * `/nodefony/<ns>/api/*` traverse, en plus du pipeline, la résolution de zone du
10
+ * firewall, un authenticator et le broker d'administration. La mesurer et la
11
+ * comparer à un handler Express nu revient à chronométrer deux choses
12
+ * différentes — et à imputer au framework le coût de son étage d'administration.
13
+ * Ce controller emprunte le chemin d'une route applicative normale : routing,
14
+ * contexte, sérialisation, réponse.
15
+ *
16
+ * **Chemin hors aire admin** : `/nodefony/kernel/bench` (deux segments, sans
17
+ * `/api/`) échappe au pattern `^/nodefony/[^/]+/api(/|$)` de la zone
18
+ * `nodefony-admin`, donc aucune zone ne s'y applique — pas de 401 à mesurer, et
19
+ * pas besoin d'ouvrir une zone dédiée. Il évite aussi le repli SPA mono-segment
20
+ * de Studio (`/nodefony/{page}`).
21
+ *
22
+ * **N'existe que sous `NF_BENCH_ROUTE=1`** : zéro surface en production par
23
+ * défaut. C'est un drapeau d'OUTILLAGE (banc), pas une option applicative — d'où
24
+ * une variable d'environnement plutôt qu'une clé de configuration.
25
+ */
26
+ declare class BenchController extends Controller {
27
+ constructor(context: ContextType);
28
+ /** Rend le corps figé. Aucune lecture de kernel, aucun I/O, aucune allocation. */
29
+ index(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
30
+ /**
31
+ * Dump de la sonde perf in-situ du http-kernel (`NF_PERF_PROBE=1`) : µs
32
+ * moyens par requête des postes enterScope / new HttpContext / leaveScope.
33
+ * Vit ICI (et pas dans `@nodefony/test`, `policy:"dev"`) parce que le décor
34
+ * de mesure est le mono `production` du banc — où le module test n'existe
35
+ * pas. `?reset=1` remet les compteurs à zéro (à faire après le warmup, pour
36
+ * ne pas diluer la mesure avec le code froid).
37
+ */
38
+ probe(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
39
+ }
40
+ /**
41
+ * Monte la route de banc — appelée par le module framework, uniquement si
42
+ * `NF_BENCH_ROUTE=1`.
43
+ */
44
+ export declare function mountBenchRoutes(frameworkModule: Module): void;
45
+ export default BenchController;