@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.
- package/LICENSE +544 -0
- package/README.md +50 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +211 -0
- package/dist/nodefony/config/config.js +61 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controller/AdminApiController.js +163 -0
- package/dist/nodefony/controller/ApiKeyController.js +151 -0
- package/dist/nodefony/controller/BenchController.js +132 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -0
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
- package/dist/nodefony/controller/SessionAuthController.js +141 -0
- package/dist/nodefony/controller/TokenAuthController.js +113 -0
- package/dist/nodefony/controller/TotpController.js +129 -0
- package/dist/nodefony/controller/WebAuthnController.js +242 -0
- package/dist/nodefony/controller/oauthAuthority.js +74 -0
- package/dist/nodefony/decorators/routerDecorators.js +967 -0
- package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
- package/dist/nodefony/interfaces/IController.js +1 -0
- package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
- package/dist/nodefony/interfaces/IResolver.js +1 -0
- package/dist/nodefony/interfaces/IRoute.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/AdminBroker.js +106 -0
- package/dist/nodefony/service/Eta.js +68 -0
- package/dist/nodefony/service/IdempotencyStore.js +136 -0
- package/dist/nodefony/service/router.js +243 -0
- package/dist/nodefony/src/Controller.js +515 -0
- package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
- package/dist/nodefony/src/KernelAdminApi.js +1243 -0
- package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
- package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
- package/dist/nodefony/src/Resolver.js +416 -0
- package/dist/nodefony/src/ResourceController.js +148 -0
- package/dist/nodefony/src/Route.js +476 -0
- package/dist/nodefony/src/SyslogAdminApi.js +466 -0
- package/dist/nodefony/src/Template.js +15 -0
- package/dist/nodefony/src/configMutation.js +186 -0
- package/dist/nodefony/src/docsReader.js +929 -0
- package/dist/nodefony/src/idempotency.js +137 -0
- package/dist/nodefony/src/idempotencyGc.js +32 -0
- package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
- package/dist/nodefony/src/scopeCatalog.js +40 -0
- package/dist/nodefony/src/syslogFilters.js +51 -0
- package/dist/types/index.d.ts +96 -0
- package/dist/types/nodefony/config/config.d.ts +41 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
- package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
- package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
- package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
- package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
- package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
- package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
- package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +5 -0
- package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
- package/dist/types/nodefony/service/Eta.d.ts +25 -0
- package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
- package/dist/types/nodefony/service/router.d.ts +53 -0
- package/dist/types/nodefony/src/Controller.d.ts +193 -0
- package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
- package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
- package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
- package/dist/types/nodefony/src/Resolver.d.ts +165 -0
- package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
- package/dist/types/nodefony/src/Route.d.ts +192 -0
- package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
- package/dist/types/nodefony/src/Template.d.ts +8 -0
- package/dist/types/nodefony/src/configMutation.d.ts +109 -0
- package/dist/types/nodefony/src/docsReader.d.ts +369 -0
- package/dist/types/nodefony/src/idempotency.d.ts +96 -0
- package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
- package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
- package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
- package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
- package/docs/admin.md +451 -0
- package/docs/controller.md +645 -0
- package/docs/decorateurs.md +845 -0
- package/docs/idempotence.md +741 -0
- package/docs/index.md +151 -0
- package/docs/routing.md +648 -0
- package/docs/templates.md +380 -0
- 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;
|