@nodefony/framework 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +50 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +211 -0
  6. package/dist/nodefony/config/config.js +61 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  8. package/dist/nodefony/controller/AdminApiController.js +163 -0
  9. package/dist/nodefony/controller/ApiKeyController.js +151 -0
  10. package/dist/nodefony/controller/BenchController.js +132 -0
  11. package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
  12. package/dist/nodefony/controller/OAuth2Controller.js +133 -0
  13. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
  14. package/dist/nodefony/controller/SessionAuthController.js +141 -0
  15. package/dist/nodefony/controller/TokenAuthController.js +113 -0
  16. package/dist/nodefony/controller/TotpController.js +129 -0
  17. package/dist/nodefony/controller/WebAuthnController.js +242 -0
  18. package/dist/nodefony/controller/oauthAuthority.js +74 -0
  19. package/dist/nodefony/decorators/routerDecorators.js +967 -0
  20. package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
  21. package/dist/nodefony/interfaces/IController.js +1 -0
  22. package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
  23. package/dist/nodefony/interfaces/IResolver.js +1 -0
  24. package/dist/nodefony/interfaces/IRoute.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/service/AdminBroker.js +106 -0
  27. package/dist/nodefony/service/Eta.js +68 -0
  28. package/dist/nodefony/service/IdempotencyStore.js +136 -0
  29. package/dist/nodefony/service/router.js +243 -0
  30. package/dist/nodefony/src/Controller.js +515 -0
  31. package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
  32. package/dist/nodefony/src/KernelAdminApi.js +1243 -0
  33. package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
  34. package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
  35. package/dist/nodefony/src/Resolver.js +416 -0
  36. package/dist/nodefony/src/ResourceController.js +148 -0
  37. package/dist/nodefony/src/Route.js +476 -0
  38. package/dist/nodefony/src/SyslogAdminApi.js +466 -0
  39. package/dist/nodefony/src/Template.js +15 -0
  40. package/dist/nodefony/src/configMutation.js +186 -0
  41. package/dist/nodefony/src/docsReader.js +929 -0
  42. package/dist/nodefony/src/idempotency.js +137 -0
  43. package/dist/nodefony/src/idempotencyGc.js +32 -0
  44. package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
  45. package/dist/nodefony/src/scopeCatalog.js +40 -0
  46. package/dist/nodefony/src/syslogFilters.js +51 -0
  47. package/dist/types/index.d.ts +96 -0
  48. package/dist/types/nodefony/config/config.d.ts +41 -0
  49. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  50. package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
  51. package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
  52. package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
  53. package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
  54. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
  55. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
  56. package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
  57. package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
  58. package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
  59. package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
  60. package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
  61. package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
  62. package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
  63. package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
  64. package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
  65. package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
  66. package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
  67. package/dist/types/nodefony/interfaces/index.d.ts +5 -0
  68. package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
  69. package/dist/types/nodefony/service/Eta.d.ts +25 -0
  70. package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
  71. package/dist/types/nodefony/service/router.d.ts +53 -0
  72. package/dist/types/nodefony/src/Controller.d.ts +193 -0
  73. package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
  74. package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
  75. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
  76. package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
  77. package/dist/types/nodefony/src/Resolver.d.ts +165 -0
  78. package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
  79. package/dist/types/nodefony/src/Route.d.ts +192 -0
  80. package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
  81. package/dist/types/nodefony/src/Template.d.ts +8 -0
  82. package/dist/types/nodefony/src/configMutation.d.ts +109 -0
  83. package/dist/types/nodefony/src/docsReader.d.ts +369 -0
  84. package/dist/types/nodefony/src/idempotency.d.ts +96 -0
  85. package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
  86. package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
  87. package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
  88. package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
  89. package/docs/admin.md +451 -0
  90. package/docs/controller.md +645 -0
  91. package/docs/decorateurs.md +845 -0
  92. package/docs/idempotence.md +741 -0
  93. package/docs/index.md +151 -0
  94. package/docs/routing.md +648 -0
  95. package/docs/templates.md +380 -0
  96. package/package.json +83 -0
@@ -0,0 +1,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 };