@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/NOTICE +7 -0
- package/README.md +32 -0
- package/TRADEMARKS.md +33 -0
- package/dist/index.d.ts +131 -26
- package/dist/index.js +339 -14
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
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 {
|
|
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
|
|
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(
|
|
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, () =>
|
|
960
|
-
if (web !== void 0) await router.middleware("web").group(() =>
|
|
961
|
-
if (api !== void 0) await router.prefix(apiPrefix).middleware("api").group(() =>
|
|
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,
|