@orieljs/routing 0.1.0-alpha.1 → 0.1.0-alpha.11

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/dist/index.js CHANGED
@@ -1,15 +1,12 @@
1
1
  // BaseRouter.ts
2
2
  import {
3
3
  CurrentRoute as CurrentRoute2,
4
+ InertiaFactory,
4
5
  UrlGenerator,
5
6
  ViewFactory
6
7
  } from "@orieljs/contracts";
7
8
  import { currentContainer as currentContainer2 } from "@orieljs/container";
8
9
 
9
- // dispatch.ts
10
- import { CurrentRoute } from "@orieljs/contracts";
11
- import { currentContainer } from "@orieljs/container";
12
-
13
10
  // bindings.ts
14
11
  import { RouteModelResolver } from "@orieljs/contracts";
15
12
  import { BindingResolutionError } from "@orieljs/container";
@@ -69,6 +66,24 @@ var RoutingError = class extends Error {
69
66
  status = 500;
70
67
  name = "RoutingError";
71
68
  };
69
+ var InvalidUrlSignatureError = class extends Error {
70
+ kind = "Http";
71
+ status = 403;
72
+ name = "InvalidUrlSignatureError";
73
+ constructor() {
74
+ super("Invalid signature.");
75
+ }
76
+ };
77
+ var ThrottleRequestsError = class extends Error {
78
+ constructor(headers = {}) {
79
+ super("Too Many Attempts.");
80
+ this.headers = headers;
81
+ }
82
+ headers;
83
+ kind = "Http";
84
+ status = 429;
85
+ name = "ThrottleRequestsError";
86
+ };
72
87
 
73
88
  // middleware.ts
74
89
  import {
@@ -237,6 +252,10 @@ async function substituteBindings(router, route, values, container) {
237
252
  return bound;
238
253
  }
239
254
 
255
+ // dispatch.ts
256
+ import { CurrentRoute } from "@orieljs/contracts";
257
+ import { currentContainer } from "@orieljs/container";
258
+
240
259
  // RouteDefinition.ts
241
260
  var PARAMETER = /\{(\w+)(?::(\w+))?(\?)?\}/g;
242
261
  var WHOLE_OPTIONAL = /^\{(\w+)(?::\w+)?\?\}$/;
@@ -327,6 +346,7 @@ var RouteDefinition = class {
327
346
  this.#name = (this.#name ?? "") + name;
328
347
  return this;
329
348
  }
349
+ /** middleware('auth', 'verified') or middleware(['auth', 'verified']). */
330
350
  middleware(...middleware) {
331
351
  this.#middleware.push(...middleware.flat());
332
352
  return this;
@@ -341,6 +361,32 @@ var RouteDefinition = class {
341
361
  this.#compiled = void 0;
342
362
  return this;
343
363
  }
364
+ /** Laravel's whereAlpha(): ASCII letters only. One parameter name or several. */
365
+ whereAlpha(parameters) {
366
+ return this.#assign(parameters, "[a-zA-Z]+");
367
+ }
368
+ /** Laravel's whereAlphaNumeric(): ASCII letters and digits. */
369
+ whereAlphaNumeric(parameters) {
370
+ return this.#assign(parameters, "[a-zA-Z0-9]+");
371
+ }
372
+ /** Laravel's whereNumber(): digits only. */
373
+ whereNumber(parameters) {
374
+ return this.#assign(parameters, "[0-9]+");
375
+ }
376
+ /** Laravel's whereUlid(): 26 Crockford base-32 characters, the first at most 7. */
377
+ whereUlid(parameters) {
378
+ return this.#assign(parameters, "[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}");
379
+ }
380
+ /** Laravel's whereUuid(): five groups of hex digits (8-4-4-4-12), any version. */
381
+ whereUuid(parameters) {
382
+ return this.#assign(parameters, "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}");
383
+ }
384
+ /** Laravel's whereIn(): one of the values. Unlike Laravel, each value is matched literally (`c++` is not a pattern). */
385
+ whereIn(parameters, values) {
386
+ const first = typeof parameters === "string" ? parameters : parameters[0];
387
+ if (values.length === 0 && first !== void 0) throw new Error(`Routing requirement for "${first}" cannot be empty.`);
388
+ return this.#assign(parameters, values.map((value) => escapeRegex(String(value))).join("|"));
389
+ }
344
390
  gatherMiddleware() {
345
391
  return [...this.#middleware];
346
392
  }
@@ -371,6 +417,10 @@ var RouteDefinition = class {
371
417
  if (this.uri === "/") return [];
372
418
  return this.uri.split("/").map((segment) => WHOLE_OPTIONAL.test(segment) ? 2 : segment.includes("{") ? 1 : 0);
373
419
  }
420
+ #assign(parameters, pattern) {
421
+ const names = typeof parameters === "string" ? [parameters] : parameters;
422
+ return this.where(Object.fromEntries(names.map((name) => [name, pattern])));
423
+ }
374
424
  #regex(patterns, version) {
375
425
  if (this.#compiled !== void 0 && this.#compiled.version === version) return this.#compiled.regex;
376
426
  const wheres = { ...patterns, ...this.#wheres };
@@ -643,6 +693,23 @@ var BaseRouter = class {
643
693
  holder.route = this.addRoute(["GET", "HEAD"], uri, action);
644
694
  return holder.route;
645
695
  }
696
+ /**
697
+ * Route.inertia('/about', 'About', props): GET/HEAD; renders the page through the InertiaFactory (resolved when
698
+ * the route runs, so Routing needs no Inertia import). Route parameters are merged into the props.
699
+ */
700
+ inertia(uri, component, props = {}) {
701
+ const holder = {};
702
+ const action = (...values) => {
703
+ const parameters = Object.fromEntries(holder.route.parameters.map((parameter, index) => [parameter.name, values[index]]));
704
+ const container = currentContainer2() ?? this.container;
705
+ if (container === void 0 || !container.bound(InertiaFactory)) {
706
+ throw new RoutingError(`Cannot render Inertia page [${component}]: no InertiaFactory is bound.`);
707
+ }
708
+ return container.make(InertiaFactory).render(component, { ...props, ...parameters });
709
+ };
710
+ holder.route = this.addRoute(["GET", "HEAD"], uri, action);
711
+ return holder.route;
712
+ }
646
713
  resource(name, controller, options = {}) {
647
714
  this.#registerResource(name, controller, options, false);
648
715
  }
@@ -713,6 +780,14 @@ var BaseRouter = class {
713
780
  getModel(parameter) {
714
781
  return this.#models.get(parameter);
715
782
  }
783
+ /**
784
+ * Binds a matched route's raw parameter values as a request to it would: Route.bind() callbacks, Route.model(),
785
+ * then implicit binding through the RouteModelResolver of `container`. A missing model throws
786
+ * RouteModelNotFoundError (404). Live uses it to check a page route's `can:` middleware again on each update.
787
+ */
788
+ substituteBindings(route, values, container) {
789
+ return substituteBindings(this, route, values, container);
790
+ }
716
791
  aliasMiddleware(name, middleware) {
717
792
  this.#aliases[name] = middleware;
718
793
  return this;
@@ -777,7 +852,8 @@ var BaseRouter = class {
777
852
  };
778
853
 
779
854
  // BaseUrlGenerator.ts
780
- import { ConfigRepository, HttpRequest, Session } from "@orieljs/contracts";
855
+ import { createHmac, timingSafeEqual } from "crypto";
856
+ import { ConfigRepository, Encrypter, HttpRequest, Session } from "@orieljs/contracts";
781
857
  import { currentContainer as currentContainer3 } from "@orieljs/container";
782
858
  var VALID_URL = /^(#|\/\/|https?:\/\/|(mailto|tel|sms):)/i;
783
859
  var isPlainObject = (value) => {
@@ -805,6 +881,17 @@ function routeKey(value, field) {
805
881
  if (field !== void 0) return typeof model.getAttribute === "function" ? model.getAttribute(field) : model[field];
806
882
  return typeof model.getRouteKey === "function" ? model.getRouteKey() : value;
807
883
  }
884
+ function sign(url, key) {
885
+ return createHmac("sha256", key).update(url).digest("hex");
886
+ }
887
+ function safeEquals(expected, given) {
888
+ const a = Buffer.from(expected);
889
+ const b = Buffer.from(given);
890
+ return a.length === b.length && timingSafeEqual(a, b);
891
+ }
892
+ function availableAt(expiration) {
893
+ return expiration instanceof Date ? Math.floor(expiration.getTime() / 1e3) : Math.floor(Date.now() / 1e3) + expiration;
894
+ }
808
895
  var BaseUrlGenerator = class {
809
896
  constructor(router, container) {
810
897
  this.router = router;
@@ -892,6 +979,69 @@ var BaseUrlGenerator = class {
892
979
  hasRoute(name) {
893
980
  return this.router.getByName(name) !== void 0;
894
981
  }
982
+ /**
983
+ * Laravel's URL::signedRoute(): the route URL with its parameters sorted by name, then a `signature` query (the
984
+ * HMAC-SHA256 of that URL, keyed by APP_KEY). `expiration` (a Date, or seconds from now) adds `expires`.
985
+ * `absolute: false` signs the path only (checked by `signed:relative`).
986
+ */
987
+ signedRoute(name, parameters, expiration, absolute = true) {
988
+ const named = this.#namedParameters(name, parameters);
989
+ for (const reserved of ["signature", "expires"]) {
990
+ if (Object.hasOwn(named, reserved)) {
991
+ throw new RoutingError(`"${reserved}" is a reserved parameter when generating signed routes. Please rename your route parameter.`);
992
+ }
993
+ }
994
+ if (expiration !== void 0) named.expires = availableAt(expiration);
995
+ const sorted = Object.fromEntries(Object.keys(named).sort().map((key) => [key, named[key]]));
996
+ const signature = sign(this.route(name, sorted, absolute), this.#signingKeys()[0]);
997
+ return this.route(name, { ...sorted, signature }, absolute);
998
+ }
999
+ /** Laravel's URL::temporarySignedRoute(): a signed URL that expires at `expiration` (a Date, or seconds from now). */
1000
+ temporarySignedRoute(name, expiration, parameters, absolute = true) {
1001
+ return this.signedRoute(name, parameters, expiration, absolute);
1002
+ }
1003
+ /** The request URL carries a correct signature (current or previous APP_KEY) that hasn't expired. */
1004
+ hasValidSignature(request, absolute = true) {
1005
+ return this.hasCorrectSignature(request, absolute) && this.signatureHasNotExpired(request);
1006
+ }
1007
+ /** The signature matches the URL without its `signature` query, in a constant-time comparison. */
1008
+ hasCorrectSignature(request, absolute = true) {
1009
+ const full = new URL(request.fullUrl());
1010
+ const path = request.path();
1011
+ const url = absolute ? request.url() : path === "/" ? "/" : `/${path}`;
1012
+ const query = full.search.slice(1).split("&").filter((pair) => pair !== "" && pair.split("=")[0] !== "signature").join("&");
1013
+ const original = query === "" ? url : `${url}?${query}`;
1014
+ const given = full.searchParams.get("signature") ?? "";
1015
+ return this.#signingKeys().some((key) => safeEquals(sign(original, key), given));
1016
+ }
1017
+ /** No `expires` query, or one that is still in the future. */
1018
+ signatureHasNotExpired(request) {
1019
+ const expires = new URL(request.fullUrl()).searchParams.get("expires");
1020
+ if (expires === null || expires === "") return true;
1021
+ const at = Number(expires);
1022
+ return Number.isFinite(at) && Math.floor(Date.now() / 1e3) <= at;
1023
+ }
1024
+ /** Named parameters: an object as is, positional values (a list or one value) by the route's parameter names. */
1025
+ #namedParameters(name, parameters) {
1026
+ if (isPlainObject(parameters)) return { ...parameters };
1027
+ const route = this.router.getByName(name);
1028
+ if (route === void 0) throw new RouteNotDefinedError(name);
1029
+ const values = parameters === void 0 || parameters === null ? [] : Array.isArray(parameters) ? parameters : [parameters];
1030
+ const named = {};
1031
+ route.parameters.forEach((parameter, index) => {
1032
+ if (index < values.length) named[parameter.name] = values[index];
1033
+ });
1034
+ return named;
1035
+ }
1036
+ /** The Encrypter's key, then its previous keys (config app.previous_keys), so rotated keys still verify. */
1037
+ #signingKeys() {
1038
+ const scope = this.#scope();
1039
+ if (scope === void 0 || !scope.bound(Encrypter)) {
1040
+ throw new RoutingError("Signed URLs need the Encrypter: set APP_KEY (oriel key:generate).");
1041
+ }
1042
+ const encrypter = scope.make(Encrypter);
1043
+ return typeof encrypter.getAllKeys === "function" ? encrypter.getAllKeys() : [encrypter.getKey()];
1044
+ }
895
1045
  #join(path, absolute) {
896
1046
  const trimmed = path.replace(/^\/+|\/+$/g, "");
897
1047
  if (!absolute) return `/${trimmed}`;
@@ -916,6 +1066,51 @@ import { Router } from "@orieljs/contracts";
916
1066
  import { facade } from "@orieljs/support";
917
1067
  var Route = facade(Router);
918
1068
 
1069
+ // health.ts
1070
+ import { ConfigRepository as ConfigRepository2, EventDispatcher, ExceptionHandler as ExceptionHandler2 } from "@orieljs/contracts";
1071
+ var DiagnosingHealth = class {
1072
+ };
1073
+ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
1074
+ var STYLE = ':root{color-scheme:dark}*{box-sizing:border-box;margin:0}html{background:#05050b}body{min-height:100vh;display:grid;place-items:center;padding:20px;background:#1a1a1a;color:#fff;font:400 16px/1.7 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}main{display:flex;align-items:center;gap:20px;width:100%;max-width:640px;padding:20px 32px;border-radius:18px;background:#212121}.dot{flex:none;width:12px;height:12px;border-radius:50%;background:#74ec5a}.dot.down{background:#ff3543}h1{font-size:21px;line-height:1.4;font-weight:600}p{color:#a3a3a3}.time{color:#ba94ff}';
1075
+ function healthResponse(page = {}) {
1076
+ const healthy = page.healthy ?? true;
1077
+ const time = page.milliseconds === void 0 ? "" : ` Response rendered in <span class="time">${Math.round(page.milliseconds)} ms</span>.`;
1078
+ const html2 = `<!DOCTYPE html>
1079
+ <html lang="en">
1080
+ <head>
1081
+ <meta charset="utf-8">
1082
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1083
+ <title>${escapeHtml(page.appName ?? "Oriel")}</title>
1084
+ <style>${STYLE}</style>
1085
+ </head>
1086
+ <body>
1087
+ <main>
1088
+ <span class="${healthy ? "dot" : "dot down"}" aria-hidden="true"></span>
1089
+ <div>
1090
+ <h1>Application ${healthy ? "up" : "experiencing problems"}</h1>
1091
+ <p>HTTP request received.${time}</p>
1092
+ </div>
1093
+ </main>
1094
+ </body>
1095
+ </html>
1096
+ `;
1097
+ return new Response(html2, { status: healthy ? 200 : 500, headers: { "content-type": "text/html; charset=utf-8" } });
1098
+ }
1099
+ async function diagnoseHealth(app) {
1100
+ const started = performance.now();
1101
+ const config = app.bound(ConfigRepository2) ? app.make(ConfigRepository2) : void 0;
1102
+ let healthy = true;
1103
+ try {
1104
+ if (app.bound(EventDispatcher)) await app.make(EventDispatcher).dispatch(new DiagnosingHealth());
1105
+ } catch (error) {
1106
+ if (config?.get("app.debug", false) === true) throw error;
1107
+ if (app.bound(ExceptionHandler2)) await app.make(ExceptionHandler2).report(error);
1108
+ healthy = false;
1109
+ }
1110
+ const appName = String(config?.get("app.name", "Oriel") ?? "Oriel");
1111
+ return healthResponse({ healthy, appName, milliseconds: performance.now() - started });
1112
+ }
1113
+
919
1114
  // routeFiles.ts
920
1115
  import { pathToFileURL } from "url";
921
1116
  var generation = 0;
@@ -927,18 +1122,137 @@ async function loadRoutesFrom(app, router, file) {
927
1122
  ));
928
1123
  if (typeof module.default === "function") await app.run(() => module.default(router));
929
1124
  }
930
- function healthResponse() {
931
- const html2 = '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>Application up</title>\n</head>\n<body><h1>Application up</h1></body>\n</html>';
932
- return new Response(html2, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
933
- }
934
1125
 
935
1126
  // RoutingServiceProvider.ts
936
- import { Router as Router2, RoutingPaths, UrlGenerator as UrlGenerator2 } from "@orieljs/contracts";
1127
+ import { Router as Router2, RoutingPaths, UrlGenerator as UrlGenerator3 } from "@orieljs/contracts";
937
1128
  import { ServiceProvider } from "@orieljs/support";
1129
+
1130
+ // ThrottleRequests.ts
1131
+ import { createHash } from "crypto";
1132
+ import { RateLimiter } from "@orieljs/contracts";
1133
+ import { inject } from "@orieljs/container";
1134
+ var sha1 = (value) => createHash("sha1").update(value).digest("hex");
1135
+ function withHeaders(response, headers) {
1136
+ try {
1137
+ for (const [name, value] of Object.entries(headers)) response.headers.set(name, value);
1138
+ return response;
1139
+ } catch {
1140
+ const copy = new Headers(response.headers);
1141
+ for (const [name, value] of Object.entries(headers)) copy.set(name, value);
1142
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers: copy });
1143
+ }
1144
+ }
1145
+ var ThrottleRequests = class {
1146
+ async handle(request, next, maxAttempts = "60", decayMinutes = "1", prefix = "") {
1147
+ const max = Number(maxAttempts);
1148
+ const decay = Number(decayMinutes);
1149
+ if (maxAttempts.trim() === "" || !Number.isInteger(max) || max < 0 || decayMinutes.trim() === "" || !Number.isFinite(decay) || decay <= 0) {
1150
+ throw new RoutingError(
1151
+ `Invalid throttle parameters "${maxAttempts},${decayMinutes}": use throttle:<maxAttempts>,<decayMinutes>[,<prefix>] (named rate limiters are not supported).`
1152
+ );
1153
+ }
1154
+ const limiter = inject(RateLimiter);
1155
+ const key = prefix + await this.resolveRequestSignature(request);
1156
+ if (await limiter.tooManyAttempts(key, max)) {
1157
+ const retryAfter = await limiter.availableIn(key);
1158
+ throw new ThrottleRequestsError({
1159
+ "X-RateLimit-Limit": String(max),
1160
+ "X-RateLimit-Remaining": "0",
1161
+ "Retry-After": String(retryAfter),
1162
+ "X-RateLimit-Reset": String(Math.floor(Date.now() / 1e3) + retryAfter)
1163
+ });
1164
+ }
1165
+ await limiter.hit(key, Math.round(decay * 60));
1166
+ const response = await next(request);
1167
+ return withHeaders(response, {
1168
+ "X-RateLimit-Limit": String(max),
1169
+ "X-RateLimit-Remaining": String(await limiter.remaining(key, max))
1170
+ });
1171
+ }
1172
+ /** The signed-in user's identifier, else the client IP, hashed (Laravel's resolveRequestSignature()). */
1173
+ async resolveRequestSignature(request) {
1174
+ const user = await request.user();
1175
+ if (user !== void 0 && user !== null && typeof user.getAuthIdentifier === "function") {
1176
+ return sha1(String(user.getAuthIdentifier()));
1177
+ }
1178
+ return sha1(`|${request.ip() ?? ""}`);
1179
+ }
1180
+ };
1181
+
1182
+ // SetCacheHeaders.ts
1183
+ import { createHash as createHash2 } from "crypto";
1184
+ var DIRECTIVES = /* @__PURE__ */ new Set([
1185
+ "public",
1186
+ "private",
1187
+ "no_cache",
1188
+ "no_store",
1189
+ "no_transform",
1190
+ "must_revalidate",
1191
+ "proxy_revalidate",
1192
+ "immutable",
1193
+ "max_age",
1194
+ "s_maxage",
1195
+ "stale_while_revalidate",
1196
+ "stale_if_error"
1197
+ ]);
1198
+ var SetCacheHeaders = class {
1199
+ async handle(request, next, ...parameters) {
1200
+ const response = await next(request);
1201
+ const method = request.method();
1202
+ if (method !== "GET" && method !== "HEAD") return response;
1203
+ if (response.headers.has("set-cookie") || /\bno-store\b/i.test(response.headers.get("cache-control") ?? "")) return response;
1204
+ const options = parameters.join(",").split(";").map((option) => option.trim()).filter((option) => option !== "");
1205
+ const headers = new Headers(response.headers);
1206
+ const directives = [];
1207
+ let etag = false;
1208
+ for (const option of options) {
1209
+ const [name = "", value] = option.split("=", 2);
1210
+ if (name === "etag") {
1211
+ etag = true;
1212
+ continue;
1213
+ }
1214
+ if (!DIRECTIVES.has(name)) throw new Error(`cache.headers: unknown option "${name}".`);
1215
+ const directive = name.replace(/_/g, "-");
1216
+ directives.push(value === void 0 ? directive : `${directive}=${value}`);
1217
+ }
1218
+ if (directives.length > 0) headers.set("cache-control", directives.join(", "));
1219
+ if (!etag) return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
1220
+ const body = new Uint8Array(await response.arrayBuffer());
1221
+ const tag = `"${createHash2("md5").update(body).digest("hex")}"`;
1222
+ headers.set("etag", tag);
1223
+ if (matches(request.header("if-none-match"), tag)) {
1224
+ const notModified = new Headers({ etag: tag });
1225
+ const cacheControl = headers.get("cache-control");
1226
+ if (cacheControl !== null) notModified.set("cache-control", cacheControl);
1227
+ return new Response(null, { status: 304, headers: notModified });
1228
+ }
1229
+ return new Response(body, { status: response.status, statusText: response.statusText, headers });
1230
+ }
1231
+ };
1232
+ function matches(header, tag) {
1233
+ if (header === void 0) return false;
1234
+ return header.split(",").some((candidate) => {
1235
+ const value = candidate.trim();
1236
+ return value === "*" || value === tag || value === `W/${tag}`;
1237
+ });
1238
+ }
1239
+
1240
+ // ValidateSignature.ts
1241
+ import { UrlGenerator as UrlGenerator2 } from "@orieljs/contracts";
1242
+ import { inject as inject2 } from "@orieljs/container";
1243
+ var ValidateSignature = class {
1244
+ async handle(request, next, ...parameters) {
1245
+ const absolute = parameters[0] !== "relative";
1246
+ if (inject2(UrlGenerator2).hasValidSignature(request, absolute)) return next(request);
1247
+ throw new InvalidUrlSignatureError();
1248
+ }
1249
+ };
1250
+
1251
+ // RoutingServiceProvider.ts
938
1252
  var RoutingServiceProvider = class extends ServiceProvider {
939
1253
  register() {
940
1254
  this.app.singleton(Router2, () => new BaseRouter(this.app));
941
- this.app.singleton(UrlGenerator2, () => {
1255
+ this.app.singleton(UrlGenerator3, () => {
942
1256
  const router = this.app.make(Router2);
943
1257
  if (!(router instanceof BaseRouter)) {
944
1258
  throw new RoutingError("The UrlGenerator needs the BaseRouter: the bound Router was replaced by another implementation.");
@@ -952,21 +1266,28 @@ var RoutingServiceProvider = class extends ServiceProvider {
952
1266
  * when the route files run.
953
1267
  */
954
1268
  boot() {
1269
+ this.app.make(Router2).aliasMiddleware("signed", ValidateSignature).aliasMiddleware("throttle", ThrottleRequests).aliasMiddleware("cache.headers", SetCacheHeaders);
955
1270
  this.app.booted(async () => {
956
1271
  if (!this.app.bound(RoutingPaths)) return;
957
1272
  const { web, api, health, apiPrefix } = this.app.make(RoutingPaths);
958
1273
  const router = this.app.make(Router2);
959
- if (health !== void 0) router.get(health, () => healthResponse());
960
- if (web !== void 0) await router.middleware("web").group(() => loadRoutesFrom(this.app, router, web));
961
- if (api !== void 0) await router.prefix(apiPrefix).middleware("api").group(() => loadRoutesFrom(this.app, router, api));
1274
+ if (health !== void 0) router.get(health, () => diagnoseHealth(this.app));
1275
+ if (web !== void 0) await router.middleware("web").group(() => this.#load(router, web));
1276
+ if (api !== void 0) await router.prefix(apiPrefix).middleware("api").group(() => this.#load(router, api));
962
1277
  });
963
1278
  }
1279
+ /** One file or several, in order: each is imported afresh for this application (a nested import is not). */
1280
+ async #load(router, files) {
1281
+ for (const file of [files].flat()) await loadRoutesFrom(this.app, router, file);
1282
+ }
964
1283
  };
965
1284
  export {
966
1285
  BaseRouter,
967
1286
  BaseUrlGenerator,
968
1287
  ControllerMethodNotFoundError,
1288
+ DiagnosingHealth,
969
1289
  GroupRegistrar,
1290
+ InvalidUrlSignatureError,
970
1291
  MethodNotAllowedHttpError,
971
1292
  MiddlewareNotFoundError,
972
1293
  NotFoundHttpError,
@@ -976,7 +1297,11 @@ export {
976
1297
  RouteNotDefinedError,
977
1298
  RoutingError,
978
1299
  RoutingServiceProvider,
1300
+ SetCacheHeaders,
1301
+ ThrottleRequests,
1302
+ ThrottleRequestsError,
979
1303
  UrlGenerationError,
1304
+ ValidateSignature,
980
1305
  buildQuery,
981
1306
  compareRoutes,
982
1307
  dispatchRoute,