@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,476 @@
|
|
|
1
|
+
import { escapeRegExp, stripTrailingSlashes, typeOf } from "nodefony";
|
|
2
|
+
import { HttpError, compileDomainPatterns, isDomainAllowed } from "@nodefony/http";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
//#region nodefony/src/Route.ts
|
|
5
|
+
/**
|
|
6
|
+
* Motif d'un segment variable : `{id}`, `/{slug}?`, `.{format}`, `{id}(\d+)`.
|
|
7
|
+
*
|
|
8
|
+
* Les deux quantificateurs sont **bornés**, et ce n'est pas de la coquetterie :
|
|
9
|
+
* `[^}]+` suivi d'un `}` obligatoire fait reprendre le moteur à chaque position
|
|
10
|
+
* quand l'accolade fermante n'arrive jamais — un temps polynomial en la longueur
|
|
11
|
+
* du chemin (`{{{{{{…`). Ici l'entrée est une route DÉCLARÉE par le développeur,
|
|
12
|
+
* donc la dénégation de service n'est pas atteignable depuis une requête ; mais
|
|
13
|
+
* ceci est une bibliothèque, et rien ne garantit qu'aucune application ne
|
|
14
|
+
* fabriquera un jour une route à partir d'une donnée qu'elle n'a pas écrite.
|
|
15
|
+
*
|
|
16
|
+
* Les bornes sont larges au point d'être insensibles : un nom de variable de
|
|
17
|
+
* plus de 128 caractères ou une contrainte de plus de 256 sont des erreurs de
|
|
18
|
+
* frappe, pas des usages. Au-delà, le segment n'est plus reconnu comme variable
|
|
19
|
+
* et le chemin est traité comme littéral — et `unreachableChars` le signale déjà
|
|
20
|
+
* au démarrage, puisqu'une accolade est un caractère qu'aucun chemin ne porte.
|
|
21
|
+
*/
|
|
22
|
+
const REG_ROUTE = /(\/)?(\.)?\{([^}]{1,128})\}(?:\(([^)]{0,256})\))?(\?)?/g;
|
|
23
|
+
const REG_REPLACE_DOUBLE_SLASH = /\/+/g;
|
|
24
|
+
/**
|
|
25
|
+
* Rend un morceau LITTÉRAL du chemin sous forme de motif — le `*` final mis à
|
|
26
|
+
* part, rien de ce que le développeur écrit ne doit valoir comme métacaractère.
|
|
27
|
+
*
|
|
28
|
+
* L'ordre importe, et c'est tout le défaut d'avant : le motif était assemblé
|
|
29
|
+
* d'abord, échappé ensuite, et seuls `/` et `.` l'étaient. Un chemin
|
|
30
|
+
* `"/a|b"` produisait donc `^\/a|b$` — qui ne reconnaît pas « /a ou /b » mais
|
|
31
|
+
* « commence par /a » **ou** « finit par b », soit `/n/importe/quoi/b` ; un
|
|
32
|
+
* `"/pricing/(beta)"` reconnaissait `/pricing/beta` et refusait le chemin
|
|
33
|
+
* déclaré. Symétriquement, la passe d'échappement mordait sur les contraintes
|
|
34
|
+
* du développeur : `{id}(\d+\.\d+)` devenait `(\d+\\.\d+)`, où `\\.` est une
|
|
35
|
+
* barre inverse littérale — la route ne reconnaissait plus rien.
|
|
36
|
+
*
|
|
37
|
+
* @param literal - le morceau de chemin situé hors de toute variable `{…}`.
|
|
38
|
+
* @param wildcard - le chemin se termine-t-il par `*` (route « fourre-tout ») ?
|
|
39
|
+
* @returns le morceau prêt à être concaténé au motif.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Caractères qu'un chemin de requête ne porte JAMAIS littéralement — l'analyseur
|
|
43
|
+
* d'URL les encode (`^` → `%5E`, `{` → `%7B`, `}` → `%7D`) ou les traite comme
|
|
44
|
+
* un délimiteur (`?` ouvre la requête, `#` le fragment, `\` est replié en `/`).
|
|
45
|
+
*
|
|
46
|
+
* Une route qui en déclare un est donc **inatteignable**. Elle l'était déjà
|
|
47
|
+
* avant que les littéraux ne soient neutralisés — à ceci près qu'elle
|
|
48
|
+
* reconnaissait alors *autre chose*, ce qui est pire. Dans les deux cas, rien
|
|
49
|
+
* ne le disait : d'où l'avertissement au démarrage.
|
|
50
|
+
*
|
|
51
|
+
* RFC 3986 §3.3 est plus stricte encore (`pchar` n'admet ni `|`, ni `[`, ni
|
|
52
|
+
* `]`), mais les analyseurs réels les laissent passer : cette liste retient ce
|
|
53
|
+
* qui est VÉRIFIÉ inatteignable, pas ce qui est interdit sur le papier.
|
|
54
|
+
*/
|
|
55
|
+
const UNREACHABLE_IN_PATHNAME = /* @__PURE__ */ new Set([
|
|
56
|
+
"^",
|
|
57
|
+
"{",
|
|
58
|
+
"}",
|
|
59
|
+
"\\",
|
|
60
|
+
"?",
|
|
61
|
+
"#"
|
|
62
|
+
]);
|
|
63
|
+
function compileLiteral(literal, wildcard) {
|
|
64
|
+
if (!wildcard) return escapeRegExp(literal);
|
|
65
|
+
return literal.split("*").map(escapeRegExp).join("(.*)/?");
|
|
66
|
+
}
|
|
67
|
+
const decode = function(str) {
|
|
68
|
+
try {
|
|
69
|
+
return decodeURIComponent(str);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return str;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Check if the variable is a default parameter.
|
|
76
|
+
* @param variable - The variable to check.
|
|
77
|
+
* @returns True if the variable is a default parameter, false otherwise.
|
|
78
|
+
*/
|
|
79
|
+
function checkDefaultParameters(variable) {
|
|
80
|
+
for (const def in this.defaults) if (def !== "controller" && def === variable) return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Callback function for the replace method in the compile function.
|
|
85
|
+
* @param _match - The matched substring.
|
|
86
|
+
* @param slash - The matched slash, if any.
|
|
87
|
+
* @param dot - The matched dot, if any.
|
|
88
|
+
* @param key - The matched key.
|
|
89
|
+
* @param capture - The matched capture group, if any.
|
|
90
|
+
* @param opt - The matched optional character, if any.
|
|
91
|
+
* @param _offset - The offset of the matched substring within the whole string being examined.
|
|
92
|
+
* @returns The replacement string.
|
|
93
|
+
*/
|
|
94
|
+
function replaceCallback(_match, slash, dot, key, capture, opt, _offset) {
|
|
95
|
+
if (this.path) {
|
|
96
|
+
this.variables.push(key);
|
|
97
|
+
if (checkDefaultParameters.call(this, key)) return `${(slash ? "\\/?" : "") + (dot ? "\\." : "")}(${capture || "[^/]*"})${opt || ""}`;
|
|
98
|
+
return `${(slash ? "\\/" : "") + (dot ? "\\." : "")}(${capture || "[^/]+"})${opt || ""}`;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Bad path `);
|
|
101
|
+
}
|
|
102
|
+
var Route = class Route {
|
|
103
|
+
name;
|
|
104
|
+
path;
|
|
105
|
+
controller;
|
|
106
|
+
classMethod;
|
|
107
|
+
prefix;
|
|
108
|
+
method;
|
|
109
|
+
schemes;
|
|
110
|
+
pattern;
|
|
111
|
+
variables = [];
|
|
112
|
+
/**
|
|
113
|
+
* Caractères du chemin déclaré qu'une requête ne peut PAS porter — la route
|
|
114
|
+
* est donc inatteignable. `undefined` tant que rien n'a été trouvé (le cas de
|
|
115
|
+
* toutes les routes saines : aucune allocation).
|
|
116
|
+
*
|
|
117
|
+
* @see {@link UNREACHABLE_IN_PATHNAME}
|
|
118
|
+
*/
|
|
119
|
+
unreachableChars;
|
|
120
|
+
defaults = {};
|
|
121
|
+
requirements = {};
|
|
122
|
+
hash;
|
|
123
|
+
host;
|
|
124
|
+
/**
|
|
125
|
+
* Patterns de domaine pré-compilés (host + `requirements.domain`), RegExp
|
|
126
|
+
* ancrées/wildcard. Compilé UNE fois dans {@link compile} ; testé par requête
|
|
127
|
+
* via {@link isDomainAllowed} (zéro alloc hot-path). `undefined` = route servie
|
|
128
|
+
* sur tous les vhosts.
|
|
129
|
+
*/
|
|
130
|
+
hostRegexp;
|
|
131
|
+
/**
|
|
132
|
+
* P3a — requirements pré-compilés au boot (0 alloc par match) :
|
|
133
|
+
* `methodsSet` = méthodes autorisées normalisées UPPERCASE (lookup O(1)) ;
|
|
134
|
+
* `methodsAllow` = valeur du header `Allow` du 405, jointe UNE fois ;
|
|
135
|
+
* `varRegexp` = requirements de variables de route (string → RegExp).
|
|
136
|
+
* `this.requirements` reste la config BRUTE (hash de route + introspection).
|
|
137
|
+
*/
|
|
138
|
+
methodsSet;
|
|
139
|
+
methodsAllow;
|
|
140
|
+
varRegexp;
|
|
141
|
+
bypassFirewall = false;
|
|
142
|
+
/**
|
|
143
|
+
* P2.9 — Cache mémoïsé : l'action attend-elle le **flux brut** du body
|
|
144
|
+
* (`@Body({ stream:true })`) ? `undefined` = pas encore calculé (résolu au 1er
|
|
145
|
+
* `routeExpectsBodyStream(route)` via lecture `Reflect` des `ParamMeta`, O(1)
|
|
146
|
+
* ensuite). Lu en amont par `handleHttp` pour sauter le parse.
|
|
147
|
+
*/
|
|
148
|
+
bodyStream;
|
|
149
|
+
/**
|
|
150
|
+
* P5 — Metadata d'action figées (`@HttpCode`/`@Header`/`@Redirect`/params/
|
|
151
|
+
* session), memo au 1er hit comme {@link bodyStream} : `undefined` = pas
|
|
152
|
+
* encore résolu (via `resolveActionMeta`, 1 lecture Reflect par route, O(1)
|
|
153
|
+
* ensuite → plus aucun `Reflect.getMetadata` par requête). Objet PARTAGÉ
|
|
154
|
+
* entre requêtes — ne jamais muter. Posé après `generateId()` → hash stable.
|
|
155
|
+
*/
|
|
156
|
+
actionMeta;
|
|
157
|
+
filePath;
|
|
158
|
+
/**
|
|
159
|
+
* Module propriétaire de la route — set par `Router.setController()` à
|
|
160
|
+
* `onBoot`, donc PAS disponible à la création de la route (via `@controller`)
|
|
161
|
+
* qui s'évalue à l'import. Utilisé pour `toLogLine()`.
|
|
162
|
+
*/
|
|
163
|
+
module;
|
|
164
|
+
constructor(name, obj) {
|
|
165
|
+
this.name = name;
|
|
166
|
+
if (obj) {
|
|
167
|
+
this.path = obj.path;
|
|
168
|
+
this.setPrefix(obj.prefix);
|
|
169
|
+
this.filePath = obj.filePath;
|
|
170
|
+
this.controller = obj.constructor;
|
|
171
|
+
this.classMethod = obj.classMethod;
|
|
172
|
+
this.method = obj.method;
|
|
173
|
+
this.setHostname(obj.host);
|
|
174
|
+
this.setDefaults(obj.defaults);
|
|
175
|
+
this.requirements = obj.requirements || {};
|
|
176
|
+
this.bypassFirewall = obj.bypassFirewall ?? false;
|
|
177
|
+
this.compile();
|
|
178
|
+
}
|
|
179
|
+
this.generateId();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Normalise le pathname de la requête pour le matching : retire le(s)
|
|
183
|
+
* slash(es) final(aux) via `stripTrailingSlashes`. À calculer UNE fois
|
|
184
|
+
* par requête dans `Router.resolve`, puis à passer à chaque {@link Route.match}
|
|
185
|
+
* scannée — sinon le getter `URL.pathname` + la normalisation sont refaits
|
|
186
|
+
* pour CHAQUE route du scan O(N) (hot path, ~N routes/req).
|
|
187
|
+
*
|
|
188
|
+
* @param context - contexte HTTP/WS courant.
|
|
189
|
+
* @returns le pathname sans slash final, ou `undefined` si la requête n'a pas d'URL.
|
|
190
|
+
*/
|
|
191
|
+
static cleanPathname(context) {
|
|
192
|
+
const req = context.request;
|
|
193
|
+
if (!req) return;
|
|
194
|
+
const p = req.pathname;
|
|
195
|
+
if (typeof p === "string") return stripTrailingSlashes(p);
|
|
196
|
+
const reqUrl = req.url;
|
|
197
|
+
if (!reqUrl) return;
|
|
198
|
+
return stripTrailingSlashes(reqUrl.pathname);
|
|
199
|
+
}
|
|
200
|
+
match(context, cleanPath, methodOverride) {
|
|
201
|
+
let res;
|
|
202
|
+
if (context.request && this.pattern) {
|
|
203
|
+
const url = cleanPath !== void 0 ? cleanPath : Route.cleanPathname(context);
|
|
204
|
+
if (url !== void 0) res = url.match(this.pattern);
|
|
205
|
+
}
|
|
206
|
+
if (!res) return res;
|
|
207
|
+
this.hydrateDefaultParameters(res);
|
|
208
|
+
this.matchHostname(context);
|
|
209
|
+
this.matchRequirements(context, methodOverride);
|
|
210
|
+
const map = [];
|
|
211
|
+
try {
|
|
212
|
+
res.slice(1).forEach((param, i) => {
|
|
213
|
+
const k = this.variables[i] || "wildcard";
|
|
214
|
+
param &&= decode(param);
|
|
215
|
+
const req = this.getRequirement(k);
|
|
216
|
+
let result = null;
|
|
217
|
+
if (req) {
|
|
218
|
+
let compiled = this.varRegexp?.[k];
|
|
219
|
+
if (!compiled) {
|
|
220
|
+
if (req instanceof RegExp) compiled = req;
|
|
221
|
+
else if (typeof req === "string") compiled = new RegExp(req);
|
|
222
|
+
else throw { BreakException: `Requirement Routing config Exception variable : ${k} must be RegExp or string : ${typeOf(req)}` };
|
|
223
|
+
}
|
|
224
|
+
result = compiled.test(param ?? "");
|
|
225
|
+
if (!result) throw { BreakException: `Requirement Exception variable : ${k} ==> ${param} doesn't match with ${String(req)}` };
|
|
226
|
+
}
|
|
227
|
+
const index = map.push(param);
|
|
228
|
+
map[k] = map[index - 1];
|
|
229
|
+
});
|
|
230
|
+
} catch (e) {
|
|
231
|
+
if (e && typeof e === "object" && "BreakException" in e) throw e.BreakException;
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
if (map && map.wildcard) map["*"] = map.wildcard;
|
|
235
|
+
return map;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Relève, dans un morceau LITTÉRAL du chemin, les caractères qu'une requête
|
|
239
|
+
* ne portera jamais. Alloue seulement s'il y en a — le cas normal ne coûte
|
|
240
|
+
* qu'un balayage.
|
|
241
|
+
*
|
|
242
|
+
* @param literal - morceau de chemin hors variable `{…}`.
|
|
243
|
+
*/
|
|
244
|
+
#collectUnreachable(literal) {
|
|
245
|
+
for (const char of literal) if (UNREACHABLE_IN_PATHNAME.has(char) && !this.unreachableChars?.includes(char)) (this.unreachableChars ??= []).push(char);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Compile the route into a regular expression pattern.
|
|
249
|
+
* @returns The compiled regular expression pattern.
|
|
250
|
+
*/
|
|
251
|
+
compile() {
|
|
252
|
+
if (!this.path) this.path = "";
|
|
253
|
+
const wildcard = this.path.endsWith("*");
|
|
254
|
+
let pattern = "";
|
|
255
|
+
let from = 0;
|
|
256
|
+
this.variables.length = 0;
|
|
257
|
+
this.unreachableChars = void 0;
|
|
258
|
+
REG_ROUTE.lastIndex = 0;
|
|
259
|
+
let found;
|
|
260
|
+
while ((found = REG_ROUTE.exec(this.path)) !== null) {
|
|
261
|
+
const literal = this.path.slice(from, found.index);
|
|
262
|
+
this.#collectUnreachable(literal);
|
|
263
|
+
pattern += compileLiteral(literal, wildcard);
|
|
264
|
+
pattern += replaceCallback.call(this, found[0], found[1], found[2], found[3], found[4], found[5], found.index);
|
|
265
|
+
from = found.index + found[0].length;
|
|
266
|
+
}
|
|
267
|
+
const tail = this.path.slice(from);
|
|
268
|
+
this.#collectUnreachable(tail);
|
|
269
|
+
pattern += compileLiteral(tail, wildcard);
|
|
270
|
+
this.pattern = new RegExp(`^${pattern}$`, "i");
|
|
271
|
+
this.compileHost();
|
|
272
|
+
this.compileRequirements();
|
|
273
|
+
return this.pattern;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pré-compile les requirements (P3a) — méthodes normalisées en Set UPPERCASE
|
|
277
|
+
* + RegExp des requirements de variables. Appelé par {@link compile} et
|
|
278
|
+
* {@link addRequirement} ; {@link matchRequirements} et la boucle de
|
|
279
|
+
* variables ne font plus que des lookups (0 alloc, 0 RegExp par match).
|
|
280
|
+
* Une RegExp string invalide throw ICI (création de la route, fail-fast)
|
|
281
|
+
* au lieu du 1er match.
|
|
282
|
+
*/
|
|
283
|
+
compileRequirements() {
|
|
284
|
+
this.methodsSet = void 0;
|
|
285
|
+
this.methodsAllow = void 0;
|
|
286
|
+
this.varRegexp = void 0;
|
|
287
|
+
const methods = this.requirements?.methods;
|
|
288
|
+
if (typeof methods === "string") {
|
|
289
|
+
const list = methods.replace(/\s/g, "").toUpperCase().split(",");
|
|
290
|
+
this.methodsSet = new Set(list);
|
|
291
|
+
this.methodsAllow = list.join(",");
|
|
292
|
+
} else if (Array.isArray(methods)) {
|
|
293
|
+
const list = methods.map((m) => String(m).toUpperCase());
|
|
294
|
+
this.methodsSet = new Set(list);
|
|
295
|
+
this.methodsAllow = list.join(",");
|
|
296
|
+
}
|
|
297
|
+
for (const k in this.requirements) {
|
|
298
|
+
if (k === "methods" || k === "domain" || k === "protocol" || k === "scheme") continue;
|
|
299
|
+
const req = this.requirements[k];
|
|
300
|
+
const compiled = req instanceof RegExp ? req : typeof req === "string" ? new RegExp(req) : null;
|
|
301
|
+
if (compiled) (this.varRegexp ??= Object.create(null))[k] = compiled;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
hydrateDefaultParameters(res) {
|
|
305
|
+
if (this.variables.length) {
|
|
306
|
+
for (let i = 0; i < this.variables.length; i++) if (this.defaults[this.variables[i]]) {
|
|
307
|
+
if (res[i + 1] === "") res[i + 1] = this.defaults[this.variables[i]];
|
|
308
|
+
}
|
|
309
|
+
} else for (const def in this.defaults) switch (def) {
|
|
310
|
+
case "controller": continue;
|
|
311
|
+
default: res.push(this.defaults[def]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
toString() {
|
|
315
|
+
return JSON.stringify(this.toObject(), null, " ");
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Formatte la route en une seule ligne lisible pour le log debug — évite le
|
|
319
|
+
* JSON multi-ligne du `toString()` qui polluait ~7 lignes par route au boot.
|
|
320
|
+
*
|
|
321
|
+
* Format : `[METHODS] path → @module/Controller.action (no auth?)`
|
|
322
|
+
*
|
|
323
|
+
* Exemple :
|
|
324
|
+
* ```
|
|
325
|
+
* [GET|HEAD] /nodefony/test/index → @test/DefaultController.index
|
|
326
|
+
* [ANY] /admin/users → @app/AdminController.users (no auth)
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* Le `@module` ne s'affiche qu'après que `Router.setController()` ait set
|
|
330
|
+
* `this.module` à `onBoot` — appel via le décorateur `@controllers` du module.
|
|
331
|
+
*/
|
|
332
|
+
toLogLine() {
|
|
333
|
+
const m = Array.isArray(this.requirements?.methods) ? this.requirements.methods.join("|") : this.requirements?.methods || this.method || "ANY";
|
|
334
|
+
const method = `[${String(m)}]`.padEnd(10);
|
|
335
|
+
const ctrl = this.controller?.name || "?";
|
|
336
|
+
const action = this.classMethod || this.name;
|
|
337
|
+
const mod = this.module?.name ? `@${this.module.name}/` : "";
|
|
338
|
+
const auth = this.bypassFirewall ? " (no auth)" : "";
|
|
339
|
+
return `${method} ${this.path} → ${mod}${ctrl}.${action}${auth}`;
|
|
340
|
+
}
|
|
341
|
+
toObject() {
|
|
342
|
+
return {
|
|
343
|
+
name: this.name,
|
|
344
|
+
path: this.path,
|
|
345
|
+
prefix: this.prefix,
|
|
346
|
+
host: this.host,
|
|
347
|
+
controller: this.defaults.controller,
|
|
348
|
+
filePath: this.filePath,
|
|
349
|
+
schemes: this.schemes,
|
|
350
|
+
variables: this.variables,
|
|
351
|
+
bypassFirewall: this.bypassFirewall
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
setDefaults(arg) {
|
|
355
|
+
if (arg) for (const ob in arg) this.addDefault(ob, arg[ob]);
|
|
356
|
+
}
|
|
357
|
+
addDefault(key, value) {
|
|
358
|
+
this.defaults[key] = value;
|
|
359
|
+
}
|
|
360
|
+
setPrefix(prefix) {
|
|
361
|
+
this.prefix = prefix;
|
|
362
|
+
this.setPattern();
|
|
363
|
+
}
|
|
364
|
+
setPattern(pattern) {
|
|
365
|
+
if (pattern) this.path = `${this.prefix}/${pattern}`;
|
|
366
|
+
else if (this.prefix) {
|
|
367
|
+
if (this.path) this.path = `${this.prefix}/${this.path}`;
|
|
368
|
+
else this.path = this.prefix;
|
|
369
|
+
}
|
|
370
|
+
return this.path = stripTrailingSlashes(this.path.replace(REG_REPLACE_DOUBLE_SLASH, "/"));
|
|
371
|
+
}
|
|
372
|
+
setHostname(hostname) {
|
|
373
|
+
this.host = hostname;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Pré-compile les patterns de domaine de la route (`host` + `requirements.domain`)
|
|
377
|
+
* en `RegExp[]` ancrées, via le matcher partagé `@nodefony/http`. Appelé par
|
|
378
|
+
* {@link compile} (kernel ↔ route = même politique). `undefined` si aucun
|
|
379
|
+
* domaine → route servie sur tous les vhosts.
|
|
380
|
+
*/
|
|
381
|
+
compileHost() {
|
|
382
|
+
const patterns = [];
|
|
383
|
+
if (this.host) patterns.push(this.host);
|
|
384
|
+
const reqDomain = this.requirements?.domain;
|
|
385
|
+
if (reqDomain) patterns.push(reqDomain);
|
|
386
|
+
this.hostRegexp = patterns.length ? compileDomainPatterns(patterns.flat()) : void 0;
|
|
387
|
+
}
|
|
388
|
+
matchHostname(context) {
|
|
389
|
+
if (this.hostRegexp) {
|
|
390
|
+
if (isDomainAllowed(this.hostRegexp, context.domain)) return true;
|
|
391
|
+
const error = new HttpError(`Domain ${context.domain} Unauthorized`);
|
|
392
|
+
error.code = 403;
|
|
393
|
+
error.type = "domain";
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Empreinte stable de la route — sert d'identité pour la comparer à
|
|
400
|
+
* elle-même (deux déclarations identiques donnent la même empreinte, deux
|
|
401
|
+
* chemins différents non). Ce n'est PAS un secret, et rien ne la vérifie :
|
|
402
|
+
* elle n'est ni persistée, ni exposée, ni transmise.
|
|
403
|
+
*
|
|
404
|
+
* SHA-256 et non MD5, bien qu'aucune propriété cryptographique ne soit
|
|
405
|
+
* requise ici : `JSON.stringify(this)` sérialise la route ENTIÈRE, donc ses
|
|
406
|
+
* `defaults` et ses `requirements` — ce que l'analyse statique lit, à juste
|
|
407
|
+
* titre, comme une donnée d'application passée dans un condensat cassé. Le
|
|
408
|
+
* coût est nul (une fois à la déclaration) et personne ne dépend de la
|
|
409
|
+
* valeur ; garder MD5 n'aurait acheté qu'une alerte à réexpliquer.
|
|
410
|
+
*
|
|
411
|
+
* @returns l'empreinte hexadécimale, également posée sur {@link hash}.
|
|
412
|
+
*/
|
|
413
|
+
generateId() {
|
|
414
|
+
this.hash = createHash("sha256").update(JSON.stringify(this)).digest("hex");
|
|
415
|
+
return this.hash;
|
|
416
|
+
}
|
|
417
|
+
addRequirement(key, value) {
|
|
418
|
+
if (key && value) {
|
|
419
|
+
this.requirements[key] = value;
|
|
420
|
+
this.compileRequirements();
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
getRequirement(key) {
|
|
425
|
+
if (key in this.requirements) return this.requirements[key];
|
|
426
|
+
}
|
|
427
|
+
hasRequirements() {
|
|
428
|
+
return Object.keys(this.requirements).length;
|
|
429
|
+
}
|
|
430
|
+
matchRequirements(context, methodOverride) {
|
|
431
|
+
if (this.hasRequirements()) for (const i in this.requirements) switch (i) {
|
|
432
|
+
case "methods":
|
|
433
|
+
if (!this.methodsSet) throw new Error(`Bad config route method : ${this.requirements[i]}`);
|
|
434
|
+
if (methodOverride !== void 0) {
|
|
435
|
+
if (!this.methodsSet.has("WEBSOCKET") || !this.methodsSet.has(methodOverride)) {
|
|
436
|
+
const error = new HttpError(`Method ${methodOverride} Unauthorized`);
|
|
437
|
+
error.code = 405;
|
|
438
|
+
error.type = "method";
|
|
439
|
+
error.allow = this.methodsAllow ?? "";
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
if (!this.methodsSet.has(context.method)) {
|
|
445
|
+
const error = new HttpError(`Method ${context.method} Unauthorized`);
|
|
446
|
+
error.code = 405;
|
|
447
|
+
error.type = "method";
|
|
448
|
+
error.allow = this.methodsAllow ?? "";
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
case "domain": break;
|
|
453
|
+
case "protocol": switch (context.method) {
|
|
454
|
+
case "WEBSOCKET":
|
|
455
|
+
let requirement = this.requirements[i];
|
|
456
|
+
if (!requirement) return true;
|
|
457
|
+
if (typeof requirement === "string") {
|
|
458
|
+
if (context.acceptedProtocol !== requirement) {
|
|
459
|
+
const error = new HttpError(`Protocol ${context.acceptedProtocol} Unauthorized`);
|
|
460
|
+
error.code = 1002;
|
|
461
|
+
error.type = "protocol";
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
const error = new HttpError(`Protocol ${context.acceptedProtocol} Unauthorized`);
|
|
466
|
+
error.code = 1002;
|
|
467
|
+
error.type = "protocol";
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
476
|
+
export { Route as default };
|