@nodefony/framework 10.0.0-alpha.9 → 10.0.0-beta.2
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/README.md +5 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +4 -4
- package/dist/nodefony/controller/AdminApiController.js +4 -3
- package/dist/nodefony/controller/ApiKeyController.js +3 -2
- package/dist/nodefony/controller/OAuth2Controller.js +11 -6
- package/dist/nodefony/controller/SessionAuthController.js +3 -3
- package/dist/nodefony/controller/TokenAuthController.js +4 -4
- package/dist/nodefony/controller/TotpController.js +7 -5
- package/dist/nodefony/controller/WebAuthnController.js +4 -4
- package/dist/nodefony/decorators/routerDecorators.js +41 -35
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -5
- package/dist/nodefony/service/router.js +78 -26
- package/dist/nodefony/src/Controller.js +184 -78
- package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
- package/dist/nodefony/src/KernelAdminApi.js +49 -49
- package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
- package/dist/nodefony/src/Resolver.js +221 -65
- package/dist/nodefony/src/Route.js +62 -39
- package/dist/nodefony/src/SyslogAdminApi.js +4 -4
- package/dist/nodefony/src/docsReader.js +49 -39
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/singletonGuard.js +76 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -1
- package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +2 -2
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
- package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
- package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
- package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
- package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
- package/dist/types/nodefony/service/router.d.ts +33 -7
- package/dist/types/nodefony/src/Controller.d.ts +97 -24
- package/dist/types/nodefony/src/Resolver.d.ts +155 -16
- package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
- package/dist/types/nodefony/src/Route.d.ts +39 -23
- package/dist/types/nodefony/src/docsReader.d.ts +19 -9
- package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
- package/docs/admin.md +1 -1
- package/docs/controller.md +139 -121
- package/docs/decorateurs.md +102 -98
- package/docs/idempotence.md +23 -21
- package/docs/index.md +1 -1
- package/docs/routing.md +60 -53
- package/docs/templates.md +14 -14
- package/package.json +10 -11
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { guardSingletonState, isDevelopment } from "./singletonGuard.js";
|
|
1
2
|
import { buildParamArgs, computeActionMeta, resolveActionMeta, resolveSessionIntent } from "../decorators/routerDecorators.js";
|
|
2
3
|
import { computeFingerprint, evaluateIdempotency, isMutationMethod, resolveIdempotencyKey, resolveIdentity } from "./idempotency.js";
|
|
3
|
-
import { RequestContext, identityHint, isArray, isPlainObject, isPromise, nodefonyError, typeOf } from "nodefony";
|
|
4
|
+
import { RequestContext, finallyMaybe, identityHint, isArray, isPlainObject, isPromise, nodefonyError, thenMaybe, typeOf } from "nodefony";
|
|
4
5
|
import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
5
6
|
//#region nodefony/src/Resolver.ts
|
|
6
7
|
/**
|
|
@@ -21,6 +22,15 @@ import { Http2Response, HttpResponse, WebsocketResponse } from "@nodefony/http";
|
|
|
21
22
|
* cohabitent dans une même suite de tests.
|
|
22
23
|
*/
|
|
23
24
|
const areaRequirements = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
/**
|
|
26
|
+
* Le controller expose-t-il un hook `initialize()` (appelé une fois à sa création) ?
|
|
27
|
+
*
|
|
28
|
+
* @param controller - instance fraîchement créée par l'injecteur.
|
|
29
|
+
* @returns `true` si `initialize` est une méthode.
|
|
30
|
+
*/
|
|
31
|
+
function hasInitialize(controller) {
|
|
32
|
+
return typeof Reflect.get(controller, "initialize") === "function";
|
|
33
|
+
}
|
|
24
34
|
var Resolver = class {
|
|
25
35
|
injector;
|
|
26
36
|
controller = null;
|
|
@@ -68,8 +78,8 @@ var Resolver = class {
|
|
|
68
78
|
this.context = context;
|
|
69
79
|
this.injector = context.container?.get("injector") ?? null;
|
|
70
80
|
}
|
|
71
|
-
match(route, context, cleanPath) {
|
|
72
|
-
const match = route.match(context, cleanPath, this.methodOverride ?? void 0);
|
|
81
|
+
match(route, context, cleanPath, literal) {
|
|
82
|
+
const match = route.match(context, cleanPath, this.methodOverride ?? void 0, literal);
|
|
73
83
|
if (match) {
|
|
74
84
|
this.variables = match;
|
|
75
85
|
this.route = route;
|
|
@@ -96,45 +106,84 @@ var Resolver = class {
|
|
|
96
106
|
* `Context.setMetaData()` pour exposer `msg.nodefony.route.variablesMap`.
|
|
97
107
|
*/
|
|
98
108
|
getMatchedParams() {
|
|
99
|
-
const
|
|
100
|
-
const params = {};
|
|
101
|
-
for (let i = 0; i < names.length; i++) params[names[i]] = this.variables[i];
|
|
109
|
+
const params = this._zipRouteParams();
|
|
102
110
|
const wildcard = this.variables["*"];
|
|
103
111
|
if (wildcard !== void 0) params["*"] = wildcard;
|
|
104
112
|
return params;
|
|
105
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Associe chaque nom de variable de la route à la valeur capturée au match.
|
|
116
|
+
*
|
|
117
|
+
* @returns la table nom → valeur (vide sans route)
|
|
118
|
+
*/
|
|
119
|
+
_zipRouteParams() {
|
|
120
|
+
const names = this.route?.variables ?? [];
|
|
121
|
+
const params = {};
|
|
122
|
+
for (let i = 0; i < names.length; i++) {
|
|
123
|
+
const name = names[i];
|
|
124
|
+
if (name !== void 0) params[name] = this.variables[i];
|
|
125
|
+
}
|
|
126
|
+
return params;
|
|
127
|
+
}
|
|
106
128
|
parsePathernController(name) {
|
|
107
|
-
let module;
|
|
108
|
-
let tab = [];
|
|
109
129
|
if (typeof name !== "string") throw new Error(`Invalid name parameter: expected a string`);
|
|
110
|
-
tab = name.split(":");
|
|
130
|
+
const tab = name.split(":");
|
|
131
|
+
const [moduleName = "", controllerName = "", actionName = ""] = tab;
|
|
111
132
|
if (tab.length !== 3) throw new Error(`Invalid name format: expected "module:controller:action"`);
|
|
112
|
-
module = this.context.kernel?.getModule(
|
|
113
|
-
if (!module) throw new Error(`Module not found: ${
|
|
114
|
-
this.controller = module.getController(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.actionName =
|
|
133
|
+
const module = this.context.kernel?.getModule(moduleName);
|
|
134
|
+
if (!module) throw new Error(`Module not found: ${moduleName}`);
|
|
135
|
+
this.controller = module.getController(controllerName);
|
|
136
|
+
const action = this.getAction(actionName);
|
|
137
|
+
if (!action) throw new Error(`Action not found in controller ${controllerName}: ${actionName}`);
|
|
138
|
+
this.action = action;
|
|
139
|
+
this.actionName = actionName;
|
|
119
140
|
this.resolve = true;
|
|
120
|
-
|
|
141
|
+
this.context.sessionIntent = resolveSessionIntent(this.controller, this.actionName);
|
|
121
142
|
}
|
|
122
143
|
getAction(name) {
|
|
123
144
|
if (!this.controller) throw new Error(`Controller not set`);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
const proto = this.controller.prototype;
|
|
146
|
+
if (!Object.hasOwn(proto, name)) return null;
|
|
147
|
+
const member = Reflect.get(proto, name);
|
|
148
|
+
return typeof member === "function" ? member : null;
|
|
127
149
|
}
|
|
128
|
-
|
|
150
|
+
/**
|
|
151
|
+
* L'instance du controller de la route, posée sur le container du contexte.
|
|
152
|
+
*
|
|
153
|
+
* Singleton (le défaut) : l'instance partagée du cache du Router — rendue
|
|
154
|
+
* DIRECTEMENT une fois créée, sans promesse à attendre (#505). Portée
|
|
155
|
+
* `request` : une instance neuve, synchrone tant que son `initialize()` ne
|
|
156
|
+
* rend pas de promesse.
|
|
157
|
+
*
|
|
158
|
+
* @param context - le contexte à injecter (celui du résolveur par défaut).
|
|
159
|
+
* @returns l'instance, ou la promesse de l'instance quand sa création attend
|
|
160
|
+
* (`initialize()` asynchrone).
|
|
161
|
+
* @throws de façon SYNCHRONE si la route n'a pas de controller, ou si sa
|
|
162
|
+
* construction lève ; un `initialize()` qui échoue rompt la promesse rendue.
|
|
163
|
+
*/
|
|
164
|
+
newController(context) {
|
|
129
165
|
if (!this.controller) throw new Error(`Route Controller not found`);
|
|
166
|
+
let created;
|
|
130
167
|
if (this.controller.scope === "singleton") {
|
|
131
168
|
const router = this.context.router;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
created = router ? router.getSingletonController(this.controller, () => this._createController(context)) : this._createController(context);
|
|
170
|
+
} else created = this._createController(context);
|
|
171
|
+
return thenMaybe(created, (controller) => this._pinController(controller));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Pose le pointeur `"controller"` sur le container du contexte : un message
|
|
175
|
+
* WS suivant ou un forward (`reload`) retrouve l'instance par ce chemin.
|
|
176
|
+
*
|
|
177
|
+
* SAUF pour une invocation par message (pont `api.request`) : le container
|
|
178
|
+
* est alors celui de la CONNEXION, et son pointeur désigne le contrôleur qui
|
|
179
|
+
* la sert (le hub). L'écraser par la cible de l'appel faisait échouer le test
|
|
180
|
+
* `instanceof` au message suivant — le hub était RÉINSTANCIÉ (DI +
|
|
181
|
+
* `initialize()`) à chaque frame qui suivait un `api.request`.
|
|
182
|
+
*
|
|
183
|
+
* @returns le controller reçu, pour enchaîner.
|
|
184
|
+
*/
|
|
185
|
+
_pinController(controller) {
|
|
186
|
+
if (!this.messageInvocation) this.context.container?.set("controller", controller);
|
|
138
187
|
return controller;
|
|
139
188
|
}
|
|
140
189
|
/**
|
|
@@ -143,19 +192,36 @@ var Resolver = class {
|
|
|
143
192
|
* requête dans `executeAction`) puis `initialize()`. Pour un singleton,
|
|
144
193
|
* `initialize()` n'est donc appelé qu'UNE fois, à la création (sémantique
|
|
145
194
|
* boot) — le per-request y lit l'ALS s'il a besoin de la requête.
|
|
195
|
+
*
|
|
196
|
+
* @returns le controller prêt ; sa promesse seulement quand `initialize()`
|
|
197
|
+
* en rend une.
|
|
198
|
+
* @throws de façon SYNCHRONE ce que lèvent la DI, le constructeur ou un
|
|
199
|
+
* `initialize()` synchrone ; un `initialize()` asynchrone qui échoue rompt
|
|
200
|
+
* la promesse rendue.
|
|
146
201
|
*/
|
|
147
|
-
|
|
148
|
-
const ctx = context
|
|
202
|
+
_createController(context) {
|
|
203
|
+
const ctx = context ?? this.context;
|
|
149
204
|
ctx.phaseStart("initialize");
|
|
150
|
-
|
|
205
|
+
return finallyMaybe(() => {
|
|
151
206
|
const controller = this.injector?.instantiate(this.controller, ctx);
|
|
152
207
|
if (!controller) throw new Error(`Route Controller not found`);
|
|
153
208
|
if (this.controller?.prototype.module) controller.module = this.controller.prototype.module;
|
|
154
|
-
|
|
155
|
-
return controller;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
209
|
+
const initialized = hasInitialize(controller) ? controller.initialize() : void 0;
|
|
210
|
+
return thenMaybe(initialized, () => this._guardSingleton(controller, ctx));
|
|
211
|
+
}, () => ctx.phaseEnd("initialize"));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Filet de DÉVELOPPEMENT : un singleton sert toutes les requêtes, donc une
|
|
215
|
+
* écriture sur l'un de ses champs, à partir d'ici, fuirait vers la suivante.
|
|
216
|
+
* Posé APRÈS `initialize()`, qui prépare légitimement un état de démarrage.
|
|
217
|
+
* Jamais hors développement : les champs y deviennent des accesseurs, un coût
|
|
218
|
+
* que la production ne paie pas.
|
|
219
|
+
*
|
|
220
|
+
* @returns le controller reçu, pour enchaîner.
|
|
221
|
+
*/
|
|
222
|
+
_guardSingleton(controller, ctx) {
|
|
223
|
+
if (this.controller.scope === "singleton" && isDevelopment(ctx.kernel?.environment)) guardSingletonState(controller, controller.constructor.name);
|
|
224
|
+
return controller;
|
|
159
225
|
}
|
|
160
226
|
/**
|
|
161
227
|
* Exécute l'action résolue et retourne sa **valeur brute**, SANS la rendre sur
|
|
@@ -165,19 +231,72 @@ var Resolver = class {
|
|
|
165
231
|
* (`{ id, result }`, champ GraphQL…). Le pipeline HTTP/WS normal passe par
|
|
166
232
|
* {@link callController} (= `executeAction` + rendu).
|
|
167
233
|
*
|
|
234
|
+
* Synchrone tant que rien n'est à attendre (#505) : la garde d'autorisation
|
|
235
|
+
* n'est attendue que sur une route gardée, l'instance que si elle est à
|
|
236
|
+
* créer. La valeur de l'action est rendue BRUTE — une promesse si l'action
|
|
237
|
+
* est asynchrone ; c'est le rendu qui la démêle.
|
|
238
|
+
*
|
|
168
239
|
* @param data - args supplémentaires (message WS brut legacy) concaténés aux variables de route.
|
|
169
240
|
* @param reload - force `newController()` (le container peut déjà porter un AUTRE controller).
|
|
170
|
-
* @returns la valeur retournée par l'action + son `RedirectMeta` éventuel
|
|
241
|
+
* @returns la valeur retournée par l'action + son `RedirectMeta` éventuel —
|
|
242
|
+
* ou la promesse de ce couple quand la garde ou la création attendent.
|
|
243
|
+
* @throws de façon SYNCHRONE ce que lève une action synchrone (ou l'absence
|
|
244
|
+
* d'action) ; un refus d'autorisation rompt la promesse rendue.
|
|
171
245
|
*/
|
|
172
|
-
|
|
246
|
+
executeAction(data, reload = false, metaArg) {
|
|
173
247
|
const meta = metaArg ?? (this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName));
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
248
|
+
const guarded = this._enforceActionSecurity(meta);
|
|
249
|
+
if (guarded !== void 0) return guarded.then(() => this._acquireAndInvoke(meta, data, reload));
|
|
250
|
+
return this._acquireAndInvoke(meta, data, reload);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Les gardes d'autorisation d'une action : `@IsGranted`/`@RequireScope`, puis
|
|
254
|
+
* le filet de la ZONE quand aucune garde n'a décidé du RÔLE.
|
|
255
|
+
*
|
|
256
|
+
* ⚠️ Vrai du trajet HTTP SEULEMENT. Le kernel y arme la route sans instancier
|
|
257
|
+
* (`http-kernel.ts` `prepareFrontController`), donc ce court-circuit est celui
|
|
258
|
+
* de bout en bout. En **WebSocket**, le controller est instancié au HANDSHAKE
|
|
259
|
+
* — avant qu'une frame ne soit gardée : sur ce transport, un refus n'évite
|
|
260
|
+
* ni la DI ni `initialize()`, qui ont déjà tourné.
|
|
261
|
+
*
|
|
262
|
+
* @returns `undefined` quand la route n'est pas gardée (le cas nominal : ni
|
|
263
|
+
* lookup, ni attente) ; sinon la promesse du verdict, rompue par un refus.
|
|
264
|
+
*/
|
|
265
|
+
_enforceActionSecurity(meta) {
|
|
266
|
+
if (meta.security === null) return this._enforceAreaSecurity();
|
|
267
|
+
const granted = this._enforceSecurity(meta.security);
|
|
268
|
+
return meta.security.hasRoleClause ? granted : granted.then(() => this._enforceAreaSecurity());
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Filet de la ZONE : une route dont personne n'a décidé le RÔLE hérite de
|
|
272
|
+
* celui de sa zone du firewall, au lieu d'être ouverte à tout compte
|
|
273
|
+
* authentifié.
|
|
274
|
+
*
|
|
275
|
+
* @returns `undefined` sur l'immense majorité des routes (aucune zone, ou
|
|
276
|
+
* zone sans rôle) ; sinon la promesse du verdict.
|
|
277
|
+
*/
|
|
278
|
+
_enforceAreaSecurity() {
|
|
279
|
+
const area = this._areaSecurity();
|
|
280
|
+
return area !== null ? this._enforceSecurity(area) : void 0;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* L'instance qui sert l'action — celle déjà posée sur le container quand
|
|
284
|
+
* c'est la bonne classe, sinon une création —, puis l'action elle-même.
|
|
285
|
+
* N'attend que la création, et seulement quand elle attend.
|
|
286
|
+
*/
|
|
287
|
+
_acquireAndInvoke(meta, data, reload) {
|
|
288
|
+
const pinned = this.context.container?.get("controller");
|
|
289
|
+
if (!pinned || reload || this.controller && !(pinned instanceof this.controller)) return thenMaybe(this.newController(), (controller) => this._invokeAction(controller, meta, data));
|
|
290
|
+
return this._invokeAction(pinned, meta, data);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Appelle l'action sur l'instance : état de requête (portée `request`
|
|
294
|
+
* seulement), arguments, méta de réponse (`@HttpCode`, `@Header`), puis
|
|
295
|
+
* l'appel — synchrone, sa valeur rendue brute.
|
|
296
|
+
*
|
|
297
|
+
* @throws de façon SYNCHRONE ce que lève l'action, ou l'absence d'action.
|
|
298
|
+
*/
|
|
299
|
+
_invokeAction(controller, meta, data) {
|
|
181
300
|
if (this.controller?.scope !== "singleton") {
|
|
182
301
|
controller.setRoute(this.route);
|
|
183
302
|
if (this.queryOverride !== null) {
|
|
@@ -202,10 +321,22 @@ var Resolver = class {
|
|
|
202
321
|
};
|
|
203
322
|
throw new Error(`Route Action not found`);
|
|
204
323
|
}
|
|
205
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Exécute l'action PUIS rend sa valeur sur le transport.
|
|
326
|
+
*
|
|
327
|
+
* Synchrone de bout en bout quand rien n'attend — route non gardée,
|
|
328
|
+
* controller déjà créé, action synchrone, rendu synchrone (#505).
|
|
329
|
+
*
|
|
330
|
+
* @param data - args supplémentaires (message WS brut legacy).
|
|
331
|
+
* @param reload - force une nouvelle instance (forward).
|
|
332
|
+
* @returns le résultat du rendu, ou sa promesse dès qu'une étape attend.
|
|
333
|
+
* @throws de façon SYNCHRONE ce que lève une étape synchrone (action,
|
|
334
|
+
* construction) ; les échecs asynchrones rompent la promesse rendue.
|
|
335
|
+
*/
|
|
336
|
+
callController(data, reload = false) {
|
|
206
337
|
const meta = this.route ? resolveActionMeta(this.route) : computeActionMeta(this.controller, this.actionName);
|
|
207
|
-
const
|
|
208
|
-
return this._handleRedirect(result, redirectMeta);
|
|
338
|
+
const outcome = meta.idempotent !== null ? this._callWithIdempotency(meta, data, reload) : this.executeAction(data, reload, meta);
|
|
339
|
+
return thenMaybe(outcome, (done) => this._handleRedirect(done.result, done.redirectMeta));
|
|
209
340
|
}
|
|
210
341
|
/**
|
|
211
342
|
* Exécute l'action AVEC la porte d'idempotence mais SANS rendu
|
|
@@ -249,9 +380,7 @@ var Resolver = class {
|
|
|
249
380
|
if (!isMutationMethod(this.methodOverride ?? context.method)) return this.executeAction(data, reload, meta);
|
|
250
381
|
const als = RequestContext.get();
|
|
251
382
|
const httpReq = context.request;
|
|
252
|
-
const
|
|
253
|
-
const params = {};
|
|
254
|
-
for (let i = 0; i < names.length; i++) params[names[i]] = this.variables[i];
|
|
383
|
+
const params = this._zipRouteParams();
|
|
255
384
|
const body = als?.body !== void 0 ? als.body : httpReq?.queryPost ?? null;
|
|
256
385
|
const store = context.container?.get("idempotencyStore");
|
|
257
386
|
const verdict = await evaluateIdempotency({
|
|
@@ -271,7 +400,11 @@ var Resolver = class {
|
|
|
271
400
|
const { status, headers, body: memo } = verdict.response;
|
|
272
401
|
const response = context.response;
|
|
273
402
|
response?.setStatusCode(status);
|
|
274
|
-
if (headers) for (const k in headers)
|
|
403
|
+
if (headers) for (const k in headers) {
|
|
404
|
+
const value = headers[k];
|
|
405
|
+
if (value === void 0) continue;
|
|
406
|
+
response?.setHeader(k, value);
|
|
407
|
+
}
|
|
275
408
|
return {
|
|
276
409
|
result: memo,
|
|
277
410
|
redirectMeta: void 0
|
|
@@ -355,12 +488,11 @@ var Resolver = class {
|
|
|
355
488
|
const token = RequestContext.get()?.token;
|
|
356
489
|
if (!authz || token === void 0) throw new nodefonyError(this._accessDenied(!authz ? `aucun moteur d'autorisation n'est posé sur cette requête` : "aucune identité n'est résolue sur cette requête — la route est-elle couverte par une zone du firewall ?"), 403);
|
|
357
490
|
const clauses = req.clauses;
|
|
358
|
-
for (
|
|
359
|
-
const clause = clauses[i];
|
|
491
|
+
for (const clause of clauses) {
|
|
360
492
|
const subject = clause.subjectParam !== void 0 ? this._resolveSubject(clause.subjectParam) : void 0;
|
|
361
493
|
let ok = false;
|
|
362
494
|
const anyOf = clause.anyOf;
|
|
363
|
-
for (
|
|
495
|
+
for (const attribute of anyOf) if (await authz.decide(token, attribute, subject)) {
|
|
364
496
|
ok = true;
|
|
365
497
|
break;
|
|
366
498
|
}
|
|
@@ -395,17 +527,15 @@ var Resolver = class {
|
|
|
395
527
|
}
|
|
396
528
|
_buildParamArgs(metas) {
|
|
397
529
|
const httpCtx = this.context;
|
|
398
|
-
const
|
|
399
|
-
const paramsMap = {};
|
|
400
|
-
for (let i = 0; i < varNames.length; i++) paramsMap[varNames[i]] = this.variables[i];
|
|
530
|
+
const paramsMap = this._zipRouteParams();
|
|
401
531
|
const ctx = this.context;
|
|
402
532
|
return buildParamArgs(metas, {
|
|
403
533
|
paramsMap,
|
|
404
|
-
request: httpCtx
|
|
405
|
-
response: httpCtx
|
|
406
|
-
session: ctx
|
|
534
|
+
request: httpCtx.request,
|
|
535
|
+
response: httpCtx.response,
|
|
536
|
+
session: ctx.session,
|
|
407
537
|
queryOverride: this.queryOverride ?? void 0,
|
|
408
|
-
getRequestCookies: (name) => ctx
|
|
538
|
+
getRequestCookies: (name) => ctx.getRequestCookies(name)
|
|
409
539
|
});
|
|
410
540
|
}
|
|
411
541
|
/**
|
|
@@ -419,11 +549,23 @@ var Resolver = class {
|
|
|
419
549
|
const response = this.context.response;
|
|
420
550
|
if (meta.httpCode !== null) response?.setStatusCode(meta.httpCode);
|
|
421
551
|
const entries = meta.headerEntries;
|
|
422
|
-
if (entries) for (
|
|
552
|
+
if (entries) for (const [name, value] of entries) response?.setHeader(name, value);
|
|
423
553
|
}
|
|
424
|
-
|
|
554
|
+
/**
|
|
555
|
+
* Applique `@Redirect` à la valeur de l'action, puis la rend.
|
|
556
|
+
*
|
|
557
|
+
* Sans `@Redirect` (le cas nominal), rend directement. Avec, la valeur est
|
|
558
|
+
* attendue seulement si c'est une promesse — une action synchrone qui rend
|
|
559
|
+
* `undefined` redirige sans suspension.
|
|
560
|
+
*
|
|
561
|
+
* @returns le résultat du rendu, ou sa promesse.
|
|
562
|
+
*/
|
|
563
|
+
_handleRedirect(actionResult, redirectMeta) {
|
|
425
564
|
if (!redirectMeta) return this.returnController(actionResult);
|
|
426
|
-
|
|
565
|
+
return thenMaybe(actionResult, (resolved) => this._redirectOrReturn(resolved, redirectMeta));
|
|
566
|
+
}
|
|
567
|
+
/** Suite de {@link _handleRedirect}, la valeur de l'action connue. */
|
|
568
|
+
_redirectOrReturn(resolved, redirectMeta) {
|
|
427
569
|
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && "url" in resolved) {
|
|
428
570
|
const override = resolved;
|
|
429
571
|
this.context.redirect(override.url, override.statusCode ?? redirectMeta.statusCode);
|
|
@@ -435,7 +577,21 @@ var Resolver = class {
|
|
|
435
577
|
}
|
|
436
578
|
return this.returnController(resolved);
|
|
437
579
|
}
|
|
438
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Rend la valeur d'une action sur le transport, selon sa nature : promesse
|
|
582
|
+
* (démêlée puis re-rendue), texte, Buffer, réponse déjà construite, scalaire
|
|
583
|
+
* ou objet (auto-JSON), rien (l'action envoie elle-même).
|
|
584
|
+
*
|
|
585
|
+
* Pas `async` : l'envelopper dans une promesse de plus ne changeait rien au
|
|
586
|
+
* rendu, et coûtait une suspension à chaque requête (#505). La valeur n'est
|
|
587
|
+
* attendue que si c'en est une.
|
|
588
|
+
*
|
|
589
|
+
* @param result - la valeur rendue par l'action.
|
|
590
|
+
* @returns ce que rend l'envoi (souvent sa promesse), ou `undefined` quand il
|
|
591
|
+
* n'y a rien à envoyer.
|
|
592
|
+
* @throws de façon SYNCHRONE ce que lève un envoi synchrone.
|
|
593
|
+
*/
|
|
594
|
+
returnController(result) {
|
|
439
595
|
const type = typeOf(result);
|
|
440
596
|
switch (true) {
|
|
441
597
|
case result instanceof Promise:
|
|
@@ -177,7 +177,7 @@ var Route = class Route {
|
|
|
177
177
|
this.method = obj.method;
|
|
178
178
|
this.setHostname(obj.host);
|
|
179
179
|
this.setDefaults(obj.defaults);
|
|
180
|
-
this.requirements = obj.requirements
|
|
180
|
+
this.requirements = obj.requirements ?? {};
|
|
181
181
|
this.bypassFirewall = obj.bypassFirewall ?? false;
|
|
182
182
|
this.areaRoleExempt = obj.areaRoleExempt ?? false;
|
|
183
183
|
this.compile();
|
|
@@ -203,10 +203,29 @@ var Route = class Route {
|
|
|
203
203
|
if (!reqUrl) return;
|
|
204
204
|
return stripTrailingSlashes(reqUrl.pathname);
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Confronte la requête à cette route : chemin, hôte, puis exigences.
|
|
208
|
+
*
|
|
209
|
+
* @param context - contexte HTTP/WS courant.
|
|
210
|
+
* @param cleanPath - pathname déjà normalisé par l'appelant (sinon recalculé).
|
|
211
|
+
* @param methodOverride - méthode logique du pont WS-RPC (cf `Resolver`).
|
|
212
|
+
* @param literal - l'appelant GARANTIT que le chemin de la route ne porte
|
|
213
|
+
* aucune syntaxe de motif (`{}`, `*`…) — c'est le cas d'une route trouvée
|
|
214
|
+
* par l'index des littérales du `Router`. Le motif n'est alors pas exécuté
|
|
215
|
+
* quand le chemin est identique à la lettre.
|
|
216
|
+
* @returns les valeurs capturées (tableau + accès par nom), ou `null`/`undefined`
|
|
217
|
+
* quand le chemin ne correspond pas.
|
|
218
|
+
* @throws 403/405/1002 quand le chemin correspond mais pas l'hôte ou les exigences.
|
|
219
|
+
*/
|
|
220
|
+
match(context, cleanPath, methodOverride, literal) {
|
|
207
221
|
let res;
|
|
208
222
|
if (context.request && this.pattern) {
|
|
209
|
-
const url = cleanPath
|
|
223
|
+
const url = cleanPath ?? Route.cleanPathname(context);
|
|
224
|
+
if (literal === true && url === this.path && !this.#hasPositionalDefaults()) {
|
|
225
|
+
this.matchHostname(context);
|
|
226
|
+
this.matchRequirements(context, methodOverride);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
210
229
|
if (url !== void 0) res = url.match(this.pattern);
|
|
211
230
|
}
|
|
212
231
|
if (!res) return res;
|
|
@@ -228,7 +247,7 @@ var Route = class Route {
|
|
|
228
247
|
else throw { BreakException: `Requirement Routing config Exception variable : ${k} must be RegExp or string : ${typeOf(req)}` };
|
|
229
248
|
}
|
|
230
249
|
result = compiled.test(param ?? "");
|
|
231
|
-
if (!result) throw { BreakException: `Requirement Exception variable : ${k} ==> ${param} doesn't match with ${String(
|
|
250
|
+
if (!result) throw { BreakException: `Requirement Exception variable : ${k} ==> ${param} doesn't match with ${typeof req === "string" ? req : String(compiled)}` };
|
|
232
251
|
}
|
|
233
252
|
const index = map.push(param);
|
|
234
253
|
map[k] = map[index - 1];
|
|
@@ -237,7 +256,7 @@ var Route = class Route {
|
|
|
237
256
|
if (e && typeof e === "object" && "BreakException" in e) throw e.BreakException;
|
|
238
257
|
throw e;
|
|
239
258
|
}
|
|
240
|
-
if (map
|
|
259
|
+
if (map.wildcard) map["*"] = map.wildcard;
|
|
241
260
|
return map;
|
|
242
261
|
}
|
|
243
262
|
/**
|
|
@@ -264,11 +283,12 @@ var Route = class Route {
|
|
|
264
283
|
REG_ROUTE.lastIndex = 0;
|
|
265
284
|
let found;
|
|
266
285
|
while ((found = REG_ROUTE.exec(this.path)) !== null) {
|
|
286
|
+
const [match, slash, dot, key = "", capture, opt] = found;
|
|
267
287
|
const literal = this.path.slice(from, found.index);
|
|
268
288
|
this.#collectUnreachable(literal);
|
|
269
289
|
pattern += compileLiteral(literal, wildcard);
|
|
270
|
-
pattern += replaceCallback.call(this,
|
|
271
|
-
from = found.index +
|
|
290
|
+
pattern += replaceCallback.call(this, match, slash, dot, key, capture, opt, found.index);
|
|
291
|
+
from = found.index + match.length;
|
|
272
292
|
}
|
|
273
293
|
const tail = this.path.slice(from);
|
|
274
294
|
this.#collectUnreachable(tail);
|
|
@@ -290,13 +310,12 @@ var Route = class Route {
|
|
|
290
310
|
this.methodsSet = void 0;
|
|
291
311
|
this.methodsAllow = void 0;
|
|
292
312
|
this.varRegexp = void 0;
|
|
293
|
-
const methods = this.requirements
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const list = methods.map((m) => String(m).toUpperCase());
|
|
313
|
+
const methods = this.requirements.methods;
|
|
314
|
+
let list;
|
|
315
|
+
if (typeof methods === "string") list = methods.replace(/\s/g, "").toUpperCase().split(",");
|
|
316
|
+
else if (Array.isArray(methods)) list = methods.map((m) => m.toUpperCase());
|
|
317
|
+
if (list) {
|
|
318
|
+
if (list.includes("GET") && !list.includes("HEAD")) list.push("HEAD");
|
|
300
319
|
this.methodsSet = new Set(list);
|
|
301
320
|
this.methodsAllow = list.join(",");
|
|
302
321
|
}
|
|
@@ -307,12 +326,18 @@ var Route = class Route {
|
|
|
307
326
|
if (compiled) (this.varRegexp ??= Object.create(null))[k] = compiled;
|
|
308
327
|
}
|
|
309
328
|
}
|
|
329
|
+
#hasPositionalDefaults() {
|
|
330
|
+
for (const def in this.defaults) if (def !== "controller") return true;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
310
333
|
hydrateDefaultParameters(res) {
|
|
311
|
-
if (this.variables.length) {
|
|
312
|
-
|
|
313
|
-
|
|
334
|
+
if (this.variables.length) for (let i = 0; i < this.variables.length; i++) {
|
|
335
|
+
const name = this.variables[i];
|
|
336
|
+
if (name !== void 0 && this.defaults[name]) {
|
|
337
|
+
if (res[i + 1] === "") res[i + 1] = this.defaults[name];
|
|
314
338
|
}
|
|
315
|
-
}
|
|
339
|
+
}
|
|
340
|
+
else for (const def in this.defaults) switch (def) {
|
|
316
341
|
case "controller": continue;
|
|
317
342
|
default: res.push(this.defaults[def]);
|
|
318
343
|
}
|
|
@@ -336,8 +361,7 @@ var Route = class Route {
|
|
|
336
361
|
* `this.module` à `onBoot` — appel via le décorateur `@controllers` du module.
|
|
337
362
|
*/
|
|
338
363
|
toLogLine() {
|
|
339
|
-
const
|
|
340
|
-
const method = `[${String(m)}]`.padEnd(10);
|
|
364
|
+
const method = `[${Array.isArray(this.requirements.methods) ? this.requirements.methods.join("|") : this.requirements.methods || this.method || "ANY"}]`.padEnd(10);
|
|
341
365
|
const ctrl = this.controller?.name || "?";
|
|
342
366
|
const action = this.classMethod || this.name;
|
|
343
367
|
const mod = this.module?.name ? `@${this.module.name}/` : "";
|
|
@@ -388,14 +412,14 @@ var Route = class Route {
|
|
|
388
412
|
compileHost() {
|
|
389
413
|
const patterns = [];
|
|
390
414
|
if (this.host) patterns.push(this.host);
|
|
391
|
-
const reqDomain = this.requirements
|
|
415
|
+
const reqDomain = this.requirements.domain;
|
|
392
416
|
if (reqDomain) patterns.push(reqDomain);
|
|
393
417
|
this.hostRegexp = patterns.length ? compileDomainPatterns(patterns.flat()) : void 0;
|
|
394
418
|
}
|
|
395
419
|
matchHostname(context) {
|
|
396
420
|
if (this.hostRegexp) {
|
|
397
421
|
if (isDomainAllowed(this.hostRegexp, context.domain)) return true;
|
|
398
|
-
const error = new HttpError(`Domain ${context.domain}
|
|
422
|
+
const error = new HttpError(`Domain ${context.domain} Forbidden for this route`);
|
|
399
423
|
error.code = 403;
|
|
400
424
|
error.type = "domain";
|
|
401
425
|
throw error;
|
|
@@ -422,7 +446,7 @@ var Route = class Route {
|
|
|
422
446
|
return this.hash;
|
|
423
447
|
}
|
|
424
448
|
addRequirement(key, value) {
|
|
425
|
-
if (
|
|
449
|
+
if (value) {
|
|
426
450
|
this.requirements[key] = value;
|
|
427
451
|
this.compileRequirements();
|
|
428
452
|
return value;
|
|
@@ -437,10 +461,10 @@ var Route = class Route {
|
|
|
437
461
|
matchRequirements(context, methodOverride) {
|
|
438
462
|
if (this.hasRequirements()) for (const i in this.requirements) switch (i) {
|
|
439
463
|
case "methods":
|
|
440
|
-
if (!this.methodsSet) throw new Error(`Bad config route method : ${this.requirements
|
|
464
|
+
if (!this.methodsSet) throw new Error(`Bad config route method : ${String(this.requirements.methods)}`);
|
|
441
465
|
if (methodOverride !== void 0) {
|
|
442
466
|
if (!this.methodsSet.has("WEBSOCKET") || !this.methodsSet.has(methodOverride)) {
|
|
443
|
-
const error = new HttpError(`Method ${methodOverride}
|
|
467
|
+
const error = new HttpError(`Method ${methodOverride} Not Allowed`);
|
|
444
468
|
error.code = 405;
|
|
445
469
|
error.type = "method";
|
|
446
470
|
error.allow = this.methodsAllow ?? "";
|
|
@@ -449,7 +473,7 @@ var Route = class Route {
|
|
|
449
473
|
break;
|
|
450
474
|
}
|
|
451
475
|
if (!this.methodsSet.has(context.method)) {
|
|
452
|
-
const error = new HttpError(`Method ${context.method}
|
|
476
|
+
const error = new HttpError(`Method ${context.method} Not Allowed`);
|
|
453
477
|
error.code = 405;
|
|
454
478
|
error.type = "method";
|
|
455
479
|
error.allow = this.methodsAllow ?? "";
|
|
@@ -457,23 +481,22 @@ var Route = class Route {
|
|
|
457
481
|
}
|
|
458
482
|
break;
|
|
459
483
|
case "domain": break;
|
|
460
|
-
case "protocol":
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
const error = new HttpError(`Protocol ${context.acceptedProtocol} Unauthorized`);
|
|
467
|
-
error.code = 1002;
|
|
468
|
-
error.type = "protocol";
|
|
469
|
-
throw error;
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
const error = new HttpError(`Protocol ${context.acceptedProtocol} Unauthorized`);
|
|
484
|
+
case "protocol": if (context.method === "WEBSOCKET") {
|
|
485
|
+
const requirement = this.requirements[i];
|
|
486
|
+
if (!requirement) return true;
|
|
487
|
+
if (typeof requirement === "string") {
|
|
488
|
+
if (context.acceptedProtocol !== requirement) {
|
|
489
|
+
const error = new HttpError(`Protocol ${context.acceptedProtocol} Not Supported`);
|
|
473
490
|
error.code = 1002;
|
|
474
491
|
error.type = "protocol";
|
|
475
492
|
throw error;
|
|
476
493
|
}
|
|
494
|
+
} else {
|
|
495
|
+
const error = new HttpError(`Protocol ${context.acceptedProtocol} Not Supported`);
|
|
496
|
+
error.code = 1002;
|
|
497
|
+
error.type = "protocol";
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
477
500
|
}
|
|
478
501
|
}
|
|
479
502
|
return true;
|
|
@@ -36,7 +36,7 @@ function createSyslogAdminApi(syslog, options = {}) {
|
|
|
36
36
|
/** Lit un entier de query (`?limit=50`), borné, avec défaut. */
|
|
37
37
|
const intParam = (req, key, def, max) => {
|
|
38
38
|
const raw = req.query[key];
|
|
39
|
-
const v = Array.isArray(raw) ? raw
|
|
39
|
+
const v = Array.isArray(raw) ? raw.at(0) : raw;
|
|
40
40
|
const n = v !== void 0 ? Number.parseInt(v, 10) : NaN;
|
|
41
41
|
if (Number.isNaN(n) || n <= 0) return def;
|
|
42
42
|
return Math.min(n, max);
|
|
@@ -176,7 +176,7 @@ function createSyslogAdminApi(syslog, options = {}) {
|
|
|
176
176
|
driver: driver?.name ?? wanted ?? null
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
|
-
return
|
|
179
|
+
return driver.query(buildCriteria(request, ["driver", "scope"]));
|
|
180
180
|
}
|
|
181
181
|
},
|
|
182
182
|
{
|
|
@@ -233,7 +233,7 @@ function createSyslogAdminApi(syslog, options = {}) {
|
|
|
233
233
|
local: "destination locale (pas de réseau)"
|
|
234
234
|
}
|
|
235
235
|
};
|
|
236
|
-
return
|
|
236
|
+
return driver.probe();
|
|
237
237
|
}
|
|
238
238
|
},
|
|
239
239
|
{
|
|
@@ -307,7 +307,7 @@ function createSyslogAdminApi(syslog, options = {}) {
|
|
|
307
307
|
status: 400,
|
|
308
308
|
body: { error: "missing enabled (boolean)" }
|
|
309
309
|
};
|
|
310
|
-
if (body.enabled
|
|
310
|
+
if (!body.enabled) syslog.log(`sink texte « ${Syslog.logSinkName} » coupé (console/fichier)`, "NOTICE", "LOG-BACKPLANE");
|
|
311
311
|
const changed = Syslog.setSinkEnabled(body.enabled);
|
|
312
312
|
if (changed && body.enabled) syslog.log("sink texte rétabli", "NOTICE", "LOG-BACKPLANE");
|
|
313
313
|
return {
|