@orieljs/http 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1908 @@
1
+ // errors.ts
2
+ var HttpException = class extends Error {
3
+ constructor(status, message = "", headers = {}) {
4
+ super(message);
5
+ this.status = status;
6
+ this.headers = headers;
7
+ }
8
+ status;
9
+ headers;
10
+ kind = "Http";
11
+ name = "HttpException";
12
+ };
13
+ var HttpResponseException = class extends Error {
14
+ constructor(response2) {
15
+ super(`HTTP response exception (${response2.status}).`);
16
+ this.response = response2;
17
+ this.status = response2.status;
18
+ }
19
+ response;
20
+ kind = "Http";
21
+ name = "HttpResponseException";
22
+ status;
23
+ };
24
+ var HttpServerError = class extends Error {
25
+ kind = "Server";
26
+ status = 500;
27
+ name = "HttpServerError";
28
+ };
29
+
30
+ // body.ts
31
+ async function readBody(raw, limit) {
32
+ if (raw.body === null) return new Uint8Array(0);
33
+ const declared = Number(raw.headers.get("content-length") ?? NaN);
34
+ if (limit !== null && Number.isFinite(declared) && declared > limit) {
35
+ await raw.body.cancel();
36
+ throw new HttpException(413, "Content Too Large");
37
+ }
38
+ const reader = raw.body.getReader();
39
+ const chunks = [];
40
+ let size = 0;
41
+ for (; ; ) {
42
+ const { done, value } = await reader.read();
43
+ if (done) break;
44
+ size += value.byteLength;
45
+ if (limit !== null && size > limit) {
46
+ await reader.cancel();
47
+ throw new HttpException(413, "Content Too Large");
48
+ }
49
+ chunks.push(value);
50
+ }
51
+ const out = new Uint8Array(size);
52
+ let offset = 0;
53
+ for (const chunk of chunks) {
54
+ out.set(chunk, offset);
55
+ offset += chunk.byteLength;
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // context.ts
61
+ import { CookieJar, HttpRequest, Session } from "@orieljs/contracts";
62
+ import { currentContainer } from "@orieljs/container";
63
+ function requestScope() {
64
+ const container = currentContainer();
65
+ return container !== void 0 && container.bound(HttpRequest) ? container : void 0;
66
+ }
67
+ function currentRequest() {
68
+ return requestScope()?.make(HttpRequest);
69
+ }
70
+ function optionalSession() {
71
+ const scope = requestScope();
72
+ return scope?.bound(Session) ? scope.make(Session) : void 0;
73
+ }
74
+ function optionalCookieJar() {
75
+ const scope = requestScope();
76
+ return scope?.bound(CookieJar) ? scope.make(CookieJar) : void 0;
77
+ }
78
+
79
+ // HttpRedirectResponse.ts
80
+ function serializeCookie(cookie) {
81
+ const parts = [`${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`, `Path=${cookie.path}`];
82
+ if (cookie.domain !== void 0) parts.push(`Domain=${cookie.domain}`);
83
+ if (cookie.secure) parts.push("Secure");
84
+ if (cookie.httpOnly) parts.push("HttpOnly");
85
+ parts.push(`SameSite=${cookie.sameSite.charAt(0).toUpperCase()}${cookie.sameSite.slice(1)}`);
86
+ if (cookie.minutes < 0) parts.push("Max-Age=0", "Expires=Thu, 01 Jan 1970 00:00:00 GMT");
87
+ else if (cookie.minutes > 0) parts.push(`Max-Age=${Math.round(cookie.minutes * 60)}`);
88
+ return parts.join("; ");
89
+ }
90
+ var HttpRedirectResponse = class {
91
+ constructor(targetUrl, status = 302, headers) {
92
+ this.targetUrl = targetUrl;
93
+ this.status = status;
94
+ this.#headers = new Headers(headers);
95
+ }
96
+ targetUrl;
97
+ status;
98
+ #headers;
99
+ with(key, value) {
100
+ const session = optionalSession();
101
+ if (session === void 0) return this;
102
+ if (typeof key === "string") session.flash(key, value);
103
+ else for (const [name, item] of Object.entries(key)) session.flash(name, item);
104
+ return this;
105
+ }
106
+ /** Flashes `errors` as { [bag]: { field: string[] } }, keeping the other bags already flashed. */
107
+ withErrors(errors, bag = "default") {
108
+ const session = optionalSession();
109
+ if (session === void 0) return this;
110
+ const source = typeof errors.toObject === "function" ? errors.toObject() : errors;
111
+ const normalized = Object.fromEntries(
112
+ Object.entries(source).map(([field, messages]) => [field, Array.isArray(messages) ? [...messages] : [messages]])
113
+ );
114
+ const existing = session.get("errors", {});
115
+ session.flash("errors", { ...existing, [bag]: normalized });
116
+ return this;
117
+ }
118
+ /** Flashes `input`, or the current request's input (files are never part of input()). */
119
+ withInput(input) {
120
+ const session = optionalSession();
121
+ if (session === void 0) return this;
122
+ session.flashInput(input ?? currentRequest()?.input() ?? {});
123
+ return this;
124
+ }
125
+ /**
126
+ * Always a Set-Cookie header on this response (Laravel's RedirectResponse::withCookie). A bound CookieJar only
127
+ * supplies the defaults (path, domain, secure, same-site); queueing would lose the cookie outside the web group.
128
+ */
129
+ withCookie(name, value, minutes = 0) {
130
+ const jar = optionalCookieJar();
131
+ const cookie = jar !== void 0 ? jar.make(name, value, minutes) : { name, value, minutes, path: "/", secure: false, httpOnly: true, sameSite: "lax" };
132
+ this.#headers.append("Set-Cookie", serializeCookie(cookie));
133
+ return this;
134
+ }
135
+ header(name, value) {
136
+ this.#headers.set(name, value);
137
+ return this;
138
+ }
139
+ toResponse() {
140
+ const headers = new Headers(this.#headers);
141
+ headers.set("Location", this.targetUrl);
142
+ return new Response(null, { status: this.status, headers });
143
+ }
144
+ };
145
+
146
+ // HttpRedirector.ts
147
+ import { UrlGenerator } from "@orieljs/contracts";
148
+ import { inject } from "@orieljs/container";
149
+ var HttpRedirector = class {
150
+ to(path, status = 302, headers) {
151
+ return new HttpRedirectResponse(inject(UrlGenerator).to(path), status, headers);
152
+ }
153
+ route(name, parameters, status = 302) {
154
+ return new HttpRedirectResponse(inject(UrlGenerator).route(name, parameters), status);
155
+ }
156
+ back(fallback, status = 302) {
157
+ return new HttpRedirectResponse(inject(UrlGenerator).previous(fallback), status);
158
+ }
159
+ /** Pulls url.intended from the session (put there by guest()), else goes to `fallback`. */
160
+ intended(fallback = "/", status = 302) {
161
+ const path = optionalSession()?.pull("url.intended") ?? fallback;
162
+ return this.to(path, status);
163
+ }
164
+ away(url2, status = 302) {
165
+ return new HttpRedirectResponse(url2, status);
166
+ }
167
+ /** Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. */
168
+ guest(path, status = 302) {
169
+ const session = optionalSession();
170
+ const request2 = currentRequest();
171
+ if (session !== void 0) {
172
+ const intended = request2 !== void 0 && request2.method() === "GET" && !request2.expectsJson() ? request2.fullUrl() : inject(UrlGenerator).previous();
173
+ session.put("url.intended", intended);
174
+ }
175
+ return this.to(path, status);
176
+ }
177
+ setIntendedUrl(url2) {
178
+ optionalSession()?.put("url.intended", url2);
179
+ }
180
+ refresh(status = 302) {
181
+ return this.to(currentRequest()?.fullUrl() ?? inject(UrlGenerator).current(), status);
182
+ }
183
+ };
184
+
185
+ // clientAddress.ts
186
+ var KEY = /* @__PURE__ */ Symbol.for("oriel:http.client-address");
187
+ var store = globalThis[KEY] ??= /* @__PURE__ */ new WeakMap();
188
+ function setClientAddress(request2, address) {
189
+ if (address !== void 0 && address !== "") store.set(request2, address);
190
+ }
191
+ function clientAddress(request2) {
192
+ return store.get(request2);
193
+ }
194
+
195
+ // FormUploadedFile.ts
196
+ import { FilesystemManager } from "@orieljs/contracts";
197
+ import { inject as inject2 } from "@orieljs/container";
198
+ import { Str } from "@orieljs/support";
199
+
200
+ // mime.ts
201
+ var TYPES = {
202
+ avif: "image/avif",
203
+ css: "text/css",
204
+ csv: "text/csv",
205
+ doc: "application/msword",
206
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
207
+ gif: "image/gif",
208
+ gz: "application/gzip",
209
+ html: "text/html",
210
+ ico: "image/vnd.microsoft.icon",
211
+ jpg: "image/jpeg",
212
+ js: "text/javascript",
213
+ json: "application/json",
214
+ md: "text/markdown",
215
+ mp3: "audio/mpeg",
216
+ mp4: "video/mp4",
217
+ pdf: "application/pdf",
218
+ png: "image/png",
219
+ svg: "image/svg+xml",
220
+ txt: "text/plain",
221
+ wasm: "application/wasm",
222
+ webm: "video/webm",
223
+ webp: "image/webp",
224
+ woff: "font/woff",
225
+ woff2: "font/woff2",
226
+ xls: "application/vnd.ms-excel",
227
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
228
+ xml: "application/xml",
229
+ zip: "application/zip"
230
+ };
231
+ var ALIASES = { jpeg: "jpg", htm: "html", mjs: "js", text: "txt" };
232
+ var EXTENSIONS = Object.fromEntries(Object.entries(TYPES).map(([extension, type]) => [type, extension]));
233
+ function mimeFromExtension(pathOrExtension) {
234
+ const extension = pathOrExtension.slice(pathOrExtension.lastIndexOf(".") + 1).toLowerCase();
235
+ return TYPES[ALIASES[extension] ?? extension] ?? "application/octet-stream";
236
+ }
237
+ function extensionFromMime(type) {
238
+ return EXTENSIONS[type.split(";")[0].trim().toLowerCase()];
239
+ }
240
+
241
+ // FormUploadedFile.ts
242
+ var FormUploadedFile = class {
243
+ constructor(file) {
244
+ this.file = file;
245
+ this.clientName = file.name ?? "";
246
+ this.mimeType = file.type === "" ? "application/octet-stream" : file.type;
247
+ this.size = file.size;
248
+ }
249
+ file;
250
+ clientName;
251
+ mimeType;
252
+ size;
253
+ /** Guessed from the MIME type, falling back to the client's extension (Laravel's extension()). */
254
+ extension() {
255
+ return extensionFromMime(this.mimeType) ?? this.clientExtension();
256
+ }
257
+ clientExtension() {
258
+ const dot = this.clientName.lastIndexOf(".");
259
+ return dot === -1 ? "" : this.clientName.slice(dot + 1).toLowerCase();
260
+ }
261
+ /** A random 40-character name with the guessed extension. */
262
+ hashName() {
263
+ const extension = this.extension();
264
+ return extension === "" ? Str.random(40) : `${Str.random(40)}.${extension}`;
265
+ }
266
+ arrayBuffer() {
267
+ return this.file.arrayBuffer();
268
+ }
269
+ text() {
270
+ return this.file.text();
271
+ }
272
+ stream() {
273
+ return this.file.stream();
274
+ }
275
+ store(directory, disk) {
276
+ return inject2(FilesystemManager).disk(disk).putFile(directory, this);
277
+ }
278
+ storeAs(directory, name, disk) {
279
+ return inject2(FilesystemManager).disk(disk).putFileAs(directory, this, name);
280
+ }
281
+ };
282
+
283
+ // IncomingRequest.ts
284
+ import {
285
+ CurrentRoute,
286
+ Session as Session2,
287
+ ValidationFactory
288
+ } from "@orieljs/contracts";
289
+ import { currentContainer as currentContainer2, inject as inject3 } from "@orieljs/container";
290
+ import { Arr, Str as Str2 } from "@orieljs/support";
291
+
292
+ // input.ts
293
+ var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
294
+ var INTEGER_KEY = /^(?:0|[1-9]\d*)$/;
295
+ var isPlainObject = (value) => {
296
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
297
+ const prototype = Object.getPrototypeOf(value);
298
+ return prototype === Object.prototype || prototype === null;
299
+ };
300
+ function parseBracketKey(name) {
301
+ const open = name.indexOf("[");
302
+ if (open <= 0 || !name.endsWith("]")) return [name];
303
+ const segments = [name.slice(0, open)];
304
+ const rest = name.slice(open);
305
+ const matches = rest.match(/\[[^[\]]*\]/g);
306
+ if (matches === null || matches.join("") !== rest) return [name];
307
+ for (const match of matches) segments.push(match.slice(1, -1));
308
+ return segments;
309
+ }
310
+ function normalizeLists(value) {
311
+ if (Array.isArray(value)) return value.map(normalizeLists);
312
+ if (!isPlainObject(value)) return value;
313
+ const keys = Object.keys(value);
314
+ const out = {};
315
+ for (const key of keys) out[key] = normalizeLists(value[key]);
316
+ if (keys.length > 0 && keys.every((key, index) => key === String(index))) return keys.map((key) => out[key]);
317
+ return out;
318
+ }
319
+ function fromEntries(entries) {
320
+ const root = {};
321
+ const nextIndex = /* @__PURE__ */ new WeakMap();
322
+ for (const [name, value] of entries) {
323
+ const segments = parseBracketKey(name);
324
+ if (segments.some((segment) => FORBIDDEN.has(segment))) continue;
325
+ let node = root;
326
+ for (let i = 0; i < segments.length; i++) {
327
+ let segment = segments[i];
328
+ const next = nextIndex.get(node) ?? 0;
329
+ if (segment === "") {
330
+ segment = String(next);
331
+ nextIndex.set(node, next + 1);
332
+ } else if (INTEGER_KEY.test(segment) && Number(segment) >= next) {
333
+ nextIndex.set(node, Number(segment) + 1);
334
+ }
335
+ if (i === segments.length - 1) {
336
+ node[segment] = value;
337
+ break;
338
+ }
339
+ if (!isPlainObject(node[segment])) node[segment] = {};
340
+ node = node[segment];
341
+ }
342
+ }
343
+ return normalizeLists(root);
344
+ }
345
+ function sanitize(value) {
346
+ if (Array.isArray(value)) return value.map(sanitize);
347
+ if (!isPlainObject(value)) return value;
348
+ const out = {};
349
+ for (const [key, item] of Object.entries(value)) if (!FORBIDDEN.has(key)) out[key] = sanitize(item);
350
+ return out;
351
+ }
352
+ function mergeRecursive(base, override) {
353
+ const out = { ...base };
354
+ for (const [key, value] of Object.entries(override)) {
355
+ if (FORBIDDEN.has(key)) continue;
356
+ const current = out[key];
357
+ out[key] = (isPlainObject(current) || Array.isArray(current)) && (isPlainObject(value) || Array.isArray(value)) ? normalizeLists(mergeRecursive({ ...current }, { ...value })) : value;
358
+ }
359
+ return out;
360
+ }
361
+ function isBlank(value) {
362
+ if (value === null || value === void 0) return true;
363
+ if (typeof value === "string") return value.trim() === "";
364
+ if (Array.isArray(value)) return value.length === 0;
365
+ if (isPlainObject(value)) return Object.keys(value).length === 0;
366
+ return false;
367
+ }
368
+ function transformLeaves(tree, transform, prefix = "") {
369
+ const out = {};
370
+ for (const [key, value] of Object.entries(tree)) out[key] = transformValue(`${prefix}${key}`, value, transform);
371
+ return out;
372
+ }
373
+ function transformValue(path, value, transform) {
374
+ if (Array.isArray(value)) return value.map((item, index) => transformValue(`${path}.${index}`, item, transform));
375
+ if (isPlainObject(value)) return transformLeaves(value, transform, `${path}.`);
376
+ return transform(path, value);
377
+ }
378
+
379
+ // IncomingRequest.ts
380
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
381
+ var normalizeIp = (ip) => ip.trim().replace(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i, "$1");
382
+ function ipv4ToNumber(ip) {
383
+ const parts = ip.split(".");
384
+ if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part) || Number(part) > 255)) return void 0;
385
+ return parts.reduce((total, part) => total * 256 + Number(part), 0);
386
+ }
387
+ function ipMatches(ip, patterns) {
388
+ const address = normalizeIp(ip);
389
+ return patterns.some((pattern) => {
390
+ if (pattern === "*") return true;
391
+ if (!pattern.includes("/")) return normalizeIp(pattern) === address;
392
+ const [range, bits] = pattern.split("/");
393
+ const base = ipv4ToNumber(range);
394
+ const value = ipv4ToNumber(address);
395
+ const size = Number(bits);
396
+ if (base === void 0 || value === void 0 || !(size >= 0 && size <= 32)) return false;
397
+ const mask = size === 0 ? 0 : ~0 << 32 - size >>> 0;
398
+ return (base & mask) >>> 0 === (value & mask) >>> 0;
399
+ });
400
+ }
401
+ var IncomingRequest = class {
402
+ constructor(raw, options = {}) {
403
+ this.raw = raw;
404
+ this.#url = new URL(raw.url);
405
+ this.#trustedProxies = options.trustedProxies ?? [];
406
+ this.#maxBodySize = options.maxBodySize === void 0 ? 8 * 1024 * 1024 : options.maxBodySize;
407
+ this.#query = fromEntries(this.#url.searchParams.entries());
408
+ this.#method = raw.method.toUpperCase();
409
+ }
410
+ raw;
411
+ #url;
412
+ #trustedProxies;
413
+ #maxBodySize;
414
+ #query;
415
+ #body = {};
416
+ #files = {};
417
+ #content = "";
418
+ #method;
419
+ #cookies;
420
+ #loading;
421
+ #userResolver;
422
+ /** Reads and parses the body (JSON, urlencoded or multipart) once, then applies method spoofing. */
423
+ load() {
424
+ return this.#loading ??= this.#load();
425
+ }
426
+ async #load() {
427
+ const method = this.raw.method.toUpperCase();
428
+ if (method !== "GET" && method !== "HEAD" && this.raw.body !== null) {
429
+ const type = (this.raw.headers.get("content-type") ?? "").toLowerCase();
430
+ const isJson = type.includes("/json") || type.includes("+json");
431
+ const isUrlencoded = type.startsWith("application/x-www-form-urlencoded");
432
+ const isMultipart = type.startsWith("multipart/form-data");
433
+ if (isJson || isUrlencoded || isMultipart) {
434
+ const bytes = await readBody(this.raw, this.#maxBodySize);
435
+ try {
436
+ if (isJson) {
437
+ this.#content = new TextDecoder().decode(bytes);
438
+ this.#body = this.#parseJson(this.#content);
439
+ } else if (isUrlencoded) {
440
+ this.#content = new TextDecoder().decode(bytes);
441
+ this.#body = fromEntries(new URLSearchParams(this.#content).entries());
442
+ } else {
443
+ this.#parseForm(await new Response(bytes, { headers: { "content-type": this.raw.headers.get("content-type") ?? "" } }).formData());
444
+ }
445
+ } catch {
446
+ }
447
+ }
448
+ }
449
+ if (method === "POST") {
450
+ const override = this.raw.headers.get("x-http-method-override") ?? this.#string(this.#body._method) ?? this.#string(this.#query._method);
451
+ const spoofed = override?.toUpperCase();
452
+ if (spoofed !== void 0 && /^[A-Z]+$/.test(spoofed) && !["GET", "HEAD", "CONNECT", "TRACE"].includes(spoofed)) {
453
+ this.#method = spoofed;
454
+ }
455
+ }
456
+ return this;
457
+ }
458
+ #parseJson(text) {
459
+ let parsed;
460
+ try {
461
+ parsed = JSON.parse(text);
462
+ } catch {
463
+ return {};
464
+ }
465
+ if (Array.isArray(parsed)) return sanitize({ ...parsed });
466
+ return isPlainObject(parsed) ? sanitize(parsed) : {};
467
+ }
468
+ #parseForm(form) {
469
+ const fields = [];
470
+ const files = [];
471
+ for (const [name, value] of form.entries()) {
472
+ if (typeof value === "string") fields.push([name, value]);
473
+ else if (!((value.name ?? "") === "" && value.size === 0)) files.push([name, new FormUploadedFile(value)]);
474
+ }
475
+ this.#body = fromEntries(fields);
476
+ this.#files = fromEntries(files);
477
+ }
478
+ #string(value) {
479
+ return typeof value === "string" && value !== "" ? value : void 0;
480
+ }
481
+ method() {
482
+ return this.#method;
483
+ }
484
+ isMethod(method) {
485
+ return this.#method === method.toUpperCase();
486
+ }
487
+ /** The scheme and host, honouring X-Forwarded-Proto/Host from trusted proxies. */
488
+ root() {
489
+ const trusted = this.#fromTrustedProxy();
490
+ const proto = trusted ? this.raw.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() : void 0;
491
+ const host = trusted ? this.raw.headers.get("x-forwarded-host")?.split(",")[0]?.trim() : void 0;
492
+ const scheme = proto === "https" || proto === "http" ? proto : this.#url.protocol.slice(0, -1);
493
+ return `${scheme}://${host !== void 0 && host !== "" ? host : this.#url.host}`;
494
+ }
495
+ /** The URL without the query string or a trailing slash. */
496
+ url() {
497
+ return `${this.root()}${this.#url.pathname}`.replace(/\/+$/, "");
498
+ }
499
+ fullUrl() {
500
+ return this.#url.search === "" ? this.url() : `${this.url()}${this.#url.search}`;
501
+ }
502
+ /** 'posts/1', or '/' for the root. */
503
+ path() {
504
+ const trimmed = this.#url.pathname.replace(/^\/+|\/+$/g, "");
505
+ return trimmed === "" ? "/" : trimmed;
506
+ }
507
+ decodedPath() {
508
+ try {
509
+ return decodeURIComponent(this.path());
510
+ } catch {
511
+ return this.path();
512
+ }
513
+ }
514
+ secure() {
515
+ return this.root().startsWith("https:");
516
+ }
517
+ /** request.is('admin/*'): wildcard match against the decoded path. */
518
+ is(...patterns) {
519
+ const path = this.decodedPath();
520
+ return patterns.some((pattern) => {
521
+ const trimmed = pattern.replace(/^\/+|\/+$/g, "");
522
+ return Str2.is(trimmed === "" ? "/" : trimmed, path);
523
+ });
524
+ }
525
+ routeIs(...names) {
526
+ const name = this.#currentRoute()?.route.getName();
527
+ return name !== void 0 && names.some((pattern) => Str2.is(pattern, name));
528
+ }
529
+ header(name, defaultValue) {
530
+ return this.raw.headers.get(name) ?? defaultValue;
531
+ }
532
+ bearerToken() {
533
+ const header = this.header("authorization", "");
534
+ const position = header.toLowerCase().lastIndexOf("bearer ");
535
+ if (position === -1) return void 0;
536
+ const token = header.slice(position + 7).split(",")[0].trim();
537
+ return token === "" ? void 0 : token;
538
+ }
539
+ userAgent() {
540
+ return this.header("user-agent");
541
+ }
542
+ /** The socket address, or the right-most untrusted X-Forwarded-For entry when the socket is a trusted proxy. */
543
+ ip() {
544
+ const socket = clientAddress(this.raw);
545
+ if (socket === void 0) return void 0;
546
+ const address = normalizeIp(socket);
547
+ if (!this.#fromTrustedProxy()) return address;
548
+ const chain = (this.raw.headers.get("x-forwarded-for") ?? "").split(",").map((entry) => normalizeIp(entry)).filter((entry) => entry !== "");
549
+ for (let i = chain.length - 1; i >= 0; i--) if (!ipMatches(chain[i], this.#trustedProxies)) return chain[i];
550
+ return chain[0] ?? address;
551
+ }
552
+ #fromTrustedProxy() {
553
+ const socket = clientAddress(this.raw);
554
+ return socket !== void 0 && this.#trustedProxies.length > 0 && ipMatches(socket, this.#trustedProxies);
555
+ }
556
+ ajax() {
557
+ return this.header("x-requested-with") === "XMLHttpRequest";
558
+ }
559
+ pjax() {
560
+ return this.header("x-pjax") === "true";
561
+ }
562
+ /** The first Accept type is JSON. */
563
+ wantsJson() {
564
+ const first = this.header("accept", "").split(",")[0].trim();
565
+ return first.includes("/json") || first.includes("+json");
566
+ }
567
+ /** Laravel: (ajax and not pjax and accepts anything) or wantsJson. */
568
+ expectsJson() {
569
+ const first = this.header("accept", "").split(",")[0].split(";")[0].trim();
570
+ const acceptsAny = first === "" || first === "*/*" || first === "*";
571
+ return this.ajax() && !this.pjax() && acceptsAny || this.wantsJson();
572
+ }
573
+ /** The raw body text for JSON and urlencoded requests ('' otherwise; the raw stream stays unread for other types). */
574
+ content() {
575
+ return this.#content;
576
+ }
577
+ query(key, defaultValue) {
578
+ return Arr.get(this.#query, key, defaultValue);
579
+ }
580
+ /** Body wins over the query string (PHP's $body + $query). Files are not input. */
581
+ input(key, defaultValue) {
582
+ return Arr.get({ ...this.#query, ...this.#body }, key, defaultValue);
583
+ }
584
+ /** Input plus files (array_replace_recursive). */
585
+ all() {
586
+ return mergeRecursive({ ...this.#query, ...this.#body }, this.#files);
587
+ }
588
+ only(...keys) {
589
+ const all = this.all();
590
+ const out = {};
591
+ for (const key of keys.flat()) if (Arr.has(all, key)) Arr.set(out, key, Arr.get(all, key));
592
+ return out;
593
+ }
594
+ except(...keys) {
595
+ const all = structuredCloneInput(this.all());
596
+ for (const key of keys.flat()) Arr.forget(all, key);
597
+ return all;
598
+ }
599
+ has(...keys) {
600
+ const all = this.all();
601
+ return keys.flat().every((key) => Arr.has(all, key));
602
+ }
603
+ hasAny(...keys) {
604
+ const all = this.all();
605
+ return keys.flat().some((key) => Arr.has(all, key));
606
+ }
607
+ missing(...keys) {
608
+ return !this.has(...keys);
609
+ }
610
+ filled(key) {
611
+ return !isBlank(Arr.get(this.all(), key));
612
+ }
613
+ string(key, defaultValue = "") {
614
+ const value = this.input(key);
615
+ return value === void 0 || value === null ? defaultValue : String(value);
616
+ }
617
+ /** PHP's FILTER_VALIDATE_BOOLEAN: '1', 'true', 'on', 'yes' (any case) and true are true. */
618
+ boolean(key, defaultValue = false) {
619
+ const value = this.input(key);
620
+ if (value === void 0) return defaultValue;
621
+ if (typeof value === "boolean") return value;
622
+ if (typeof value === "number") return value === 1;
623
+ return typeof value === "string" && TRUE_VALUES.has(value.trim().toLowerCase());
624
+ }
625
+ /** PHP's intval: leading digits, else 0. */
626
+ integer(key, defaultValue = 0) {
627
+ const value = this.input(key);
628
+ if (value === void 0) return defaultValue;
629
+ if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : 0;
630
+ if (typeof value === "boolean") return value ? 1 : 0;
631
+ const parsed = Number.parseInt(String(value).trim(), 10);
632
+ return Number.isNaN(parsed) ? 0 : parsed;
633
+ }
634
+ /** Adds or replaces top-level body values. */
635
+ merge(values) {
636
+ this.#body = { ...this.#body, ...sanitize(values) };
637
+ return this;
638
+ }
639
+ /** Rewrites every scalar in the query and the body (TrimStrings and ConvertEmptyStringsToNull use this). */
640
+ transformInput(transform) {
641
+ this.#query = transformLeaves(this.#query, transform);
642
+ this.#body = transformLeaves(this.#body, transform);
643
+ return this;
644
+ }
645
+ file(key) {
646
+ const value = Arr.get(this.#files, key);
647
+ return value instanceof FormUploadedFile ? value : void 0;
648
+ }
649
+ hasFile(key) {
650
+ const value = Arr.get(this.#files, key);
651
+ return value instanceof FormUploadedFile || Array.isArray(value) && value.some((item) => item instanceof FormUploadedFile);
652
+ }
653
+ allFiles() {
654
+ return { ...this.#files };
655
+ }
656
+ cookie(name) {
657
+ return this.cookies()[name];
658
+ }
659
+ cookies() {
660
+ if (this.#cookies === void 0) {
661
+ const cookies = {};
662
+ for (const part of (this.raw.headers.get("cookie") ?? "").split(";")) {
663
+ const index = part.indexOf("=");
664
+ if (index === -1) continue;
665
+ const name = part.slice(0, index).trim();
666
+ if (name === "" || Object.hasOwn(cookies, name)) continue;
667
+ let value = part.slice(index + 1).trim();
668
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
669
+ try {
670
+ cookies[name] = decodeURIComponent(value);
671
+ } catch {
672
+ cookies[name] = value;
673
+ }
674
+ }
675
+ this.#cookies = cookies;
676
+ }
677
+ return { ...this.#cookies };
678
+ }
679
+ /** Replaces the parsed cookies (EncryptCookies calls this after decrypting). */
680
+ setCookies(cookies) {
681
+ this.#cookies = { ...cookies };
682
+ }
683
+ /** One route parameter (after model binding), or all of them. */
684
+ route(key) {
685
+ const parameters = this.#currentRoute()?.parameters ?? {};
686
+ return key === void 0 ? { ...parameters } : parameters[key];
687
+ }
688
+ session() {
689
+ return inject3(Session2);
690
+ }
691
+ user(guard) {
692
+ return this.#userResolver?.(guard);
693
+ }
694
+ setUserResolver(resolver) {
695
+ this.#userResolver = resolver;
696
+ }
697
+ /** Validates all() through the ValidationFactory; throws its Validation error on failure. */
698
+ validate(rules, messages, attributes) {
699
+ return inject3(ValidationFactory).make(this.all(), rules, messages, attributes).validated();
700
+ }
701
+ #currentRoute() {
702
+ const container = currentContainer2();
703
+ return container?.bound(CurrentRoute) ? container.make(CurrentRoute) : void 0;
704
+ }
705
+ };
706
+ function structuredCloneInput(tree) {
707
+ const out = {};
708
+ for (const [key, value] of Object.entries(tree)) {
709
+ out[key] = isPlainObject(value) ? structuredCloneInput(value) : Array.isArray(value) ? [...value] : value;
710
+ }
711
+ return out;
712
+ }
713
+
714
+ // helpers.ts
715
+ import {
716
+ HttpRequest as HttpRequest2,
717
+ Redirector,
718
+ ResponseFactory,
719
+ UrlGenerator as UrlGenerator2
720
+ } from "@orieljs/contracts";
721
+ import { inject as inject4 } from "@orieljs/container";
722
+ function request(key, defaultValue) {
723
+ const current = inject4(HttpRequest2);
724
+ return key === void 0 ? current : current.input(key, defaultValue);
725
+ }
726
+ function response(content, status, headers) {
727
+ const factory = inject4(ResponseFactory);
728
+ return content === void 0 ? factory : factory.make(content, status, headers);
729
+ }
730
+ function redirect(to, status) {
731
+ const redirector = inject4(Redirector);
732
+ return to === void 0 ? redirector : redirector.to(to, status);
733
+ }
734
+ function back(fallback, status) {
735
+ return inject4(Redirector).back(fallback, status);
736
+ }
737
+ function url(path, query, absolute) {
738
+ const generator = inject4(UrlGenerator2);
739
+ return path === void 0 ? generator : generator.to(path, query, absolute);
740
+ }
741
+ function route(name, parameters, absolute) {
742
+ return inject4(UrlGenerator2).route(name, parameters, absolute);
743
+ }
744
+ function abort(status, message = "", headers = {}) {
745
+ if (status instanceof Response) throw new HttpResponseException(status);
746
+ throw new HttpException(status, message, headers);
747
+ }
748
+ function abortIf(condition, status, message = "", headers = {}) {
749
+ if (condition) abort(status, message, headers);
750
+ }
751
+ function abortUnless(condition, status, message = "", headers = {}) {
752
+ if (!condition) abort(status, message, headers);
753
+ }
754
+
755
+ // HttpResponseFactory.ts
756
+ import { createReadStream } from "fs";
757
+ import { stat } from "fs/promises";
758
+ import { basename } from "path";
759
+ import { Readable } from "stream";
760
+ import { ViewFactory } from "@orieljs/contracts";
761
+ import { inject as inject5 } from "@orieljs/container";
762
+ function contentDisposition(type, name) {
763
+ const fallback = name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "\\$&");
764
+ const header = `${type}; filename="${fallback}"`;
765
+ return /^[\x20-\x7e]*$/.test(name) ? header : `${header}; filename*=UTF-8''${encodeURIComponent(name)}`;
766
+ }
767
+ var HttpResponseFactory = class {
768
+ /** Strings default to text/html; charset=utf-8, as in Laravel. */
769
+ make(content = null, status = 200, headers) {
770
+ const merged = new Headers(headers);
771
+ if (typeof content === "string" && !merged.has("content-type")) merged.set("content-type", "text/html; charset=utf-8");
772
+ return new Response(content, { status, headers: merged });
773
+ }
774
+ json(data, status = 200, headers) {
775
+ const merged = new Headers(headers);
776
+ if (!merged.has("content-type")) merged.set("content-type", "application/json");
777
+ return new Response(JSON.stringify(data === void 0 ? null : data), { status, headers: merged });
778
+ }
779
+ noContent(status = 204) {
780
+ return new Response(null, { status });
781
+ }
782
+ /** Renders a view (through the ViewFactory) into an HTML response. */
783
+ async view(name, data = {}, status = 200, headers) {
784
+ const html = await inject5(ViewFactory).make(name, data).render();
785
+ return this.make(html, status, headers);
786
+ }
787
+ /** Streams a file as an attachment. A missing file is a 404. */
788
+ download(path, name = basename(path), headers) {
789
+ return this.#file(path, "attachment", name, headers);
790
+ }
791
+ /** Streams a file for the browser to display (Laravel's response()->file()). */
792
+ file(path, headers) {
793
+ return this.#file(path, "inline", basename(path), headers);
794
+ }
795
+ stream(body, status = 200, headers) {
796
+ return new Response(body, { status, headers });
797
+ }
798
+ async #file(path, disposition, name, headers) {
799
+ const info = await stat(path).catch(() => void 0);
800
+ if (info === void 0 || !info.isFile()) throw new HttpException(404);
801
+ const merged = new Headers(headers);
802
+ if (!merged.has("content-type")) merged.set("content-type", mimeFromExtension(name));
803
+ merged.set("content-length", String(info.size));
804
+ merged.set("content-disposition", contentDisposition(disposition, name));
805
+ const body = Readable.toWeb(createReadStream(path));
806
+ return new Response(body, { status: 200, headers: merged });
807
+ }
808
+ };
809
+
810
+ // Kernel.ts
811
+ import {
812
+ ConfigRepository,
813
+ CurrentRoute as CurrentRoute2,
814
+ ExceptionHandler,
815
+ HttpRequest as HttpRequest3,
816
+ PipelineFactory,
817
+ Router
818
+ } from "@orieljs/contracts";
819
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
820
+
821
+ // middleware/ConvertEmptyStringsToNull.ts
822
+ var ConvertEmptyStringsToNull = class {
823
+ async handle(request2, next) {
824
+ if (request2 instanceof IncomingRequest) request2.transformInput((_key, value) => value === "" ? null : value);
825
+ return next(request2);
826
+ }
827
+ };
828
+
829
+ // middleware/TrimStrings.ts
830
+ var TRIM = /^[\s\uFEFF\u200B\u200E]+|[\s\uFEFF\u200B\u200E]+$/g;
831
+ var TrimStrings = class {
832
+ /** Dotted input keys left untouched. Override in a subclass to change. */
833
+ except = ["current_password", "password", "password_confirmation"];
834
+ async handle(request2, next) {
835
+ if (request2 instanceof IncomingRequest) {
836
+ request2.transformInput(
837
+ (key, value) => typeof value === "string" && !this.except.includes(key) ? value.replace(TRIM, "") : value
838
+ );
839
+ }
840
+ return next(request2);
841
+ }
842
+ };
843
+
844
+ // Kernel.ts
845
+ var plainServerError = () => new Response("Server Error", { status: 500, headers: { "content-type": "text/plain; charset=utf-8" } });
846
+ var Kernel = class {
847
+ constructor(app) {
848
+ this.app = app;
849
+ }
850
+ app;
851
+ #middleware = [TrimStrings, ConvertEmptyStringsToNull];
852
+ #requests = /* @__PURE__ */ new WeakMap();
853
+ getGlobalMiddleware() {
854
+ return [...this.#middleware];
855
+ }
856
+ setGlobalMiddleware(middleware) {
857
+ this.#middleware = [...middleware];
858
+ return this;
859
+ }
860
+ appendMiddleware(...middleware) {
861
+ for (const cls of middleware) if (!this.#middleware.includes(cls)) this.#middleware.push(cls);
862
+ return this;
863
+ }
864
+ prependMiddleware(...middleware) {
865
+ for (const cls of [...middleware].reverse()) if (!this.#middleware.includes(cls)) this.#middleware.unshift(cls);
866
+ return this;
867
+ }
868
+ hasMiddleware(middleware) {
869
+ return this.#middleware.includes(middleware);
870
+ }
871
+ /** Runs in the current request scope (Application.handle() opens one); called directly, it opens its own. */
872
+ handle(raw) {
873
+ const current = currentContainer3();
874
+ const scope = current !== void 0 && current !== this.app ? current : this.app.createScope();
875
+ return scope.run(() => this.#handle(raw, scope));
876
+ }
877
+ async #handle(raw, scope) {
878
+ let request2;
879
+ let response2;
880
+ try {
881
+ request2 = new IncomingRequest(raw, { trustedProxies: this.#trustedProxies(), maxBodySize: this.#maxBodySize() });
882
+ scope.instance(HttpRequest3, request2);
883
+ const state = { scope, request: request2, middleware: [] };
884
+ this.#requests.set(raw, state);
885
+ await request2.load();
886
+ response2 = await this.#sendThroughGlobal(state);
887
+ } catch (error) {
888
+ request2 ??= this.#bareRequest(scope, raw);
889
+ if (request2 !== void 0) {
890
+ response2 = await this.#renderLast(scope, request2, error);
891
+ } else {
892
+ await this.#report(scope, error);
893
+ response2 = plainServerError();
894
+ }
895
+ }
896
+ if (raw.method.toUpperCase() === "HEAD" && response2.body !== null) {
897
+ await response2.body.cancel().catch(() => void 0);
898
+ response2 = new Response(null, { status: response2.status, statusText: response2.statusText, headers: response2.headers });
899
+ }
900
+ return response2;
901
+ }
902
+ /** The request without options, for rendering an error raised while the real one was being built. */
903
+ #bareRequest(scope, raw) {
904
+ try {
905
+ const request2 = new IncomingRequest(raw);
906
+ scope.instance(HttpRequest3, request2);
907
+ return request2;
908
+ } catch {
909
+ return void 0;
910
+ }
911
+ }
912
+ /** Runs terminate() on the global and route middleware (inside the request's scope), then Application.terminate(), inside the same scope, so terminating callbacks see the request's bindings. */
913
+ async terminate(raw, response2) {
914
+ const state = this.#requests.get(raw);
915
+ if (state !== void 0) {
916
+ this.#requests.delete(raw);
917
+ await state.scope.run(async () => {
918
+ const route2 = state.scope.bound(CurrentRoute2) ? state.scope.make(CurrentRoute2).middleware : [];
919
+ for (const middleware of [...state.middleware, ...route2]) {
920
+ if (typeof middleware.terminate !== "function") continue;
921
+ try {
922
+ await middleware.terminate(state.request, response2);
923
+ } catch (error) {
924
+ await this.#report(state.scope, error);
925
+ }
926
+ }
927
+ await this.app.terminate();
928
+ });
929
+ return;
930
+ }
931
+ await this.app.terminate();
932
+ }
933
+ #sendThroughGlobal(state) {
934
+ const { scope, request: request2 } = state;
935
+ const pipes = this.#middleware.map((cls) => {
936
+ const instance = scope.make(cls);
937
+ state.middleware.push(instance);
938
+ return (passable, next) => this.#guard(scope, passable, () => instance.handle(passable, next));
939
+ });
940
+ return scope.make(PipelineFactory).make().send(request2).through(pipes).then((passable) => this.#guard(scope, passable, () => this.#dispatch(scope, passable)));
941
+ }
942
+ #dispatch(scope, request2) {
943
+ return scope.make(Router).dispatch(request2);
944
+ }
945
+ /** Converts a Responsable result, and renders a thrown error at this stage (Laravel's Routing\Pipeline). */
946
+ async #guard(scope, request2, run) {
947
+ try {
948
+ const result = await run();
949
+ if (result instanceof Response) return result;
950
+ const responsable = result;
951
+ if (typeof responsable?.toResponse === "function") return await responsable.toResponse(request2);
952
+ throw new HttpServerError("Middleware must return a Response.");
953
+ } catch (error) {
954
+ if (!scope.bound(ExceptionHandler)) throw error;
955
+ const handler = scope.make(ExceptionHandler);
956
+ await handler.report(error);
957
+ return handler.render(request2, error);
958
+ }
959
+ }
960
+ /** Last resort: the handler failed or isn't bound. Never throws. */
961
+ async #renderLast(scope, request2, error) {
962
+ if (scope.bound(ExceptionHandler)) {
963
+ try {
964
+ const handler = scope.make(ExceptionHandler);
965
+ await handler.report(error);
966
+ return await handler.render(request2, error);
967
+ } catch (renderError) {
968
+ console.error(renderError);
969
+ }
970
+ } else {
971
+ console.error(error);
972
+ }
973
+ return plainServerError();
974
+ }
975
+ async #report(scope, error) {
976
+ try {
977
+ if (scope.bound(ExceptionHandler)) await scope.make(ExceptionHandler).report(error);
978
+ else console.error(error);
979
+ } catch (reportError) {
980
+ console.error(reportError);
981
+ }
982
+ }
983
+ /** config('http.max_body_size'): bytes, or null for no limit. Anything else is a configuration error. */
984
+ #maxBodySize() {
985
+ if (!this.app.bound(ConfigRepository)) return 8 * 1024 * 1024;
986
+ const value = this.app.make(ConfigRepository).get("http.max_body_size", 8 * 1024 * 1024);
987
+ if (value === null) return null;
988
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
989
+ throw new HttpServerError(
990
+ `Invalid http.max_body_size ${JSON.stringify(value) ?? String(value)}: use a number of bytes (0 or more), or null for no limit.`
991
+ );
992
+ }
993
+ /** config('http.trusted_proxies'): an array, or a comma-separated string such as '*' or '10.0.0.0/8,::1'. */
994
+ #trustedProxies() {
995
+ if (!this.app.bound(ConfigRepository)) return [];
996
+ const value = this.app.make(ConfigRepository).get("http.trusted_proxies", []);
997
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
998
+ return Array.isArray(value) ? value.map(String) : [];
999
+ }
1000
+ };
1001
+
1002
+ // MiddlewareConfiguration.ts
1003
+ import {
1004
+ ConfigRepository as ConfigRepository2,
1005
+ MiddlewareCallbacks,
1006
+ Router as Router2
1007
+ } from "@orieljs/contracts";
1008
+ var MiddlewareConfiguration = class {
1009
+ constructor(kernel, router, config) {
1010
+ this.kernel = kernel;
1011
+ this.router = router;
1012
+ this.config = config;
1013
+ }
1014
+ kernel;
1015
+ router;
1016
+ config;
1017
+ append(...middleware) {
1018
+ this.kernel.appendMiddleware(...middleware);
1019
+ return this;
1020
+ }
1021
+ prepend(...middleware) {
1022
+ this.kernel.prependMiddleware(...middleware);
1023
+ return this;
1024
+ }
1025
+ web(options) {
1026
+ return this.#modifyGroup("web", options);
1027
+ }
1028
+ api(options) {
1029
+ return this.#modifyGroup("api", options);
1030
+ }
1031
+ alias(aliases) {
1032
+ for (const [name, middleware] of Object.entries(aliases)) this.router.aliasMiddleware(name, middleware);
1033
+ return this;
1034
+ }
1035
+ group(name, middleware) {
1036
+ this.router.middlewareGroup(name, middleware);
1037
+ return this;
1038
+ }
1039
+ /** Stored in config('http.csrf_except'), which the Session component's VerifyCsrfToken reads. */
1040
+ validateCsrfTokens(options) {
1041
+ const current = this.config?.get("http.csrf_except", []) ?? [];
1042
+ this.config?.set("http.csrf_except", [...current, ...options.except]);
1043
+ return this;
1044
+ }
1045
+ /** Stored in config('http.redirect_guests_to'), which the Auth middleware and the exception handler read. */
1046
+ redirectGuestsTo(url2) {
1047
+ this.config?.set("http.redirect_guests_to", url2);
1048
+ return this;
1049
+ }
1050
+ /** Laravel 11's redirectUsersTo(): stored in config('http.redirect_users_to') for Auth's RedirectIfAuthenticated. */
1051
+ redirectUsersTo(url2) {
1052
+ this.config?.set("http.redirect_users_to", url2);
1053
+ return this;
1054
+ }
1055
+ /** Laravel's order: prepends, then the group minus removals, then appends. */
1056
+ #modifyGroup(name, { append = [], prepend = [], remove = [] }) {
1057
+ const current = this.router.getMiddlewareGroups()[name] ?? [];
1058
+ const moved = /* @__PURE__ */ new Set([...append, ...prepend, ...remove]);
1059
+ this.router.middlewareGroup(name, [...prepend, ...current.filter((entry) => !moved.has(entry)), ...append]);
1060
+ return this;
1061
+ }
1062
+ };
1063
+ function applyMiddlewareCallbacks(app, kernel) {
1064
+ if (!app.bound(MiddlewareCallbacks)) return;
1065
+ const callbacks = app.make(MiddlewareCallbacks);
1066
+ if (callbacks.length === 0) return;
1067
+ const config = app.bound(ConfigRepository2) ? app.make(ConfigRepository2) : void 0;
1068
+ const configurator = new MiddlewareConfiguration(kernel, app.make(Router2), config);
1069
+ for (const callback of callbacks) callback(configurator);
1070
+ }
1071
+
1072
+ // ErrorHandler.ts
1073
+ import {
1074
+ ConfigRepository as ConfigRepository3,
1075
+ LogManager,
1076
+ Redirector as Redirector2,
1077
+ UrlGenerator as UrlGenerator3,
1078
+ ViewFactory as ViewFactory2
1079
+ } from "@orieljs/contracts";
1080
+ import { currentContainer as currentContainer4 } from "@orieljs/container";
1081
+
1082
+ // errorPages.ts
1083
+ import { readFile } from "fs/promises";
1084
+ import { fileURLToPath } from "url";
1085
+ var STATUS_TEXTS = {
1086
+ 400: "Bad Request",
1087
+ 401: "Unauthorized",
1088
+ 402: "Payment Required",
1089
+ 403: "Forbidden",
1090
+ 404: "Not Found",
1091
+ 405: "Method Not Allowed",
1092
+ 408: "Request Timeout",
1093
+ 409: "Conflict",
1094
+ 410: "Gone",
1095
+ 413: "Content Too Large",
1096
+ 415: "Unsupported Media Type",
1097
+ 419: "Page Expired",
1098
+ 422: "Unprocessable Content",
1099
+ 429: "Too Many Requests",
1100
+ 500: "Server Error",
1101
+ 502: "Bad Gateway",
1102
+ 503: "Service Unavailable",
1103
+ 504: "Gateway Timeout"
1104
+ };
1105
+ function statusText(status) {
1106
+ return STATUS_TEXTS[status] ?? (status >= 500 ? "Server Error" : "Error");
1107
+ }
1108
+ function escapeHtml(value) {
1109
+ return value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
1110
+ }
1111
+ function minimalPage(status, message) {
1112
+ const title = escapeHtml(message);
1113
+ return `<!DOCTYPE html>
1114
+ <html lang="en">
1115
+ <head>
1116
+ <meta charset="utf-8">
1117
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1118
+ <title>${title}</title>
1119
+ <style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#f7fafc;color:#1a202c;display:flex;align-items:center;justify-content:center;min-height:100vh}.code{padding:0 1rem;border-right:1px solid #a0aec0;font-size:1.125rem}.message{padding:0 1rem;font-size:1.125rem;text-transform:uppercase;letter-spacing:.05em}</style>
1120
+ </head>
1121
+ <body><div class="code">${status}</div><div class="message">${title}</div></body>
1122
+ </html>`;
1123
+ }
1124
+ function stackFrames(error) {
1125
+ const frames = [];
1126
+ for (const raw of (error.stack ?? "").split("\n").slice(1)) {
1127
+ const text = raw.trim();
1128
+ const match = /(?:\(|@|at )((?:file:\/\/)?(?:\/|[A-Za-z]:\\)[^()]*?):(\d+):(\d+)\)?$/.exec(text);
1129
+ if (match === null) continue;
1130
+ const location = match[1];
1131
+ const file = location.startsWith("file://") ? fileURLToPath(location) : location;
1132
+ frames.push({ file, line: Number(match[2]), column: Number(match[3]), text });
1133
+ }
1134
+ return frames;
1135
+ }
1136
+ async function excerpt(frame, radius = 5) {
1137
+ const source = await readFile(frame.file, "utf8").catch(() => void 0);
1138
+ if (source === void 0) return void 0;
1139
+ const lines = source.split("\n");
1140
+ const start = Math.max(1, frame.line - radius);
1141
+ const end = Math.min(lines.length, frame.line + radius);
1142
+ const rows = [];
1143
+ for (let number = start; number <= end; number++) {
1144
+ const current = number === frame.line ? ' class="current"' : "";
1145
+ rows.push(`<tr${current}><td class="n">${number}</td><td><code>${escapeHtml(lines[number - 1] ?? "")}</code></td></tr>`);
1146
+ }
1147
+ return `<table class="source">${rows.join("")}</table>`;
1148
+ }
1149
+ async function debugPage(error, method, url2) {
1150
+ const err = error instanceof Error ? error : new Error(String(error));
1151
+ const frames = stackFrames(err);
1152
+ const appFrame = frames.find((frame) => !frame.file.includes("node_modules") && !frame.file.startsWith("node:")) ?? frames[0];
1153
+ const source = appFrame === void 0 ? void 0 : await excerpt(appFrame);
1154
+ const causes = [];
1155
+ for (let cause = err.cause, depth = 0; cause !== void 0 && depth < 5; cause = cause.cause, depth++) {
1156
+ causes.push(`<li>${escapeHtml(cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause))}</li>`);
1157
+ }
1158
+ return `<!DOCTYPE html>
1159
+ <html lang="en">
1160
+ <head>
1161
+ <meta charset="utf-8">
1162
+ <title>${escapeHtml(err.message || err.name)}</title>
1163
+ <style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#f3f4f6;color:#111827}main{max-width:1100px;margin:2rem auto;padding:0 1rem}header{background:#fff;border-radius:.5rem;padding:1.5rem;box-shadow:0 1px 3px #0001}.class{color:#dc2626;font-weight:600}.message{font-size:1.5rem;margin:.5rem 0}.request{color:#6b7280}section{background:#fff;border-radius:.5rem;padding:1rem;margin-top:1rem;box-shadow:0 1px 3px #0001;overflow:auto}.source td{padding:0 .5rem;white-space:pre;font-size:.85rem}.source .n{color:#9ca3af;text-align:right}.source .current{background:#fee2e2}ol{font-family:ui-monospace,monospace;font-size:.85rem}</style>
1164
+ </head>
1165
+ <body><main>
1166
+ <header><div class="class">${escapeHtml(err.name)}</div><div class="message">${escapeHtml(err.message)}</div><div class="request">${escapeHtml(method)} ${escapeHtml(url2)}</div></header>
1167
+ ${appFrame === void 0 ? "" : `<section><div>${escapeHtml(appFrame.file)}:${appFrame.line}</div>${source ?? ""}</section>`}
1168
+ <section><ol>${frames.map((frame) => `<li>${escapeHtml(frame.text)}</li>`).join("")}</ol></section>
1169
+ ${causes.length === 0 ? "" : `<section><div>Caused by</div><ul>${causes.join("")}</ul></section>`}
1170
+ </main></body>
1171
+ </html>`;
1172
+ }
1173
+
1174
+ // ErrorHandler.ts
1175
+ var NOT_REPORTED = /* @__PURE__ */ new Set(["Http", "ModelNotFound", "Validation", "Authentication", "Authorization", "TokenMismatch"]);
1176
+ var HTTP_KINDS = /* @__PURE__ */ new Set(["Http", "ModelNotFound", "Authorization", "TokenMismatch"]);
1177
+ var kindOf = (error) => typeof error === "object" && error !== null && typeof error.kind === "string" ? error.kind : void 0;
1178
+ function statusFor(error) {
1179
+ const status = error?.status;
1180
+ switch (kindOf(error)) {
1181
+ case "Http":
1182
+ return status ?? 500;
1183
+ case "ModelNotFound":
1184
+ return 404;
1185
+ case "Authorization":
1186
+ return status ?? 403;
1187
+ case "TokenMismatch":
1188
+ return 419;
1189
+ case "Authentication":
1190
+ return 401;
1191
+ case "Validation":
1192
+ return status ?? 422;
1193
+ case "Server":
1194
+ return status ?? 500;
1195
+ default:
1196
+ return 500;
1197
+ }
1198
+ }
1199
+ function summarizeValidation(errors) {
1200
+ const messages = Object.values(errors).flat();
1201
+ if (messages.length === 0) return "The given data was invalid.";
1202
+ const more = messages.length - 1;
1203
+ return more === 0 ? messages[0] : `${messages[0]} (and ${more} more ${more === 1 ? "error" : "errors"})`;
1204
+ }
1205
+ var isHttpLike = (error) => {
1206
+ const kind = kindOf(error);
1207
+ return kind !== void 0 && HTTP_KINDS.has(kind);
1208
+ };
1209
+ var ErrorHandler = class {
1210
+ constructor(app) {
1211
+ this.app = app;
1212
+ }
1213
+ app;
1214
+ /** Input keys never flashed back on a validation redirect. */
1215
+ dontFlash = ["current_password", "password", "password_confirmation"];
1216
+ #reportCallbacks = [];
1217
+ #renderCallbacks = [];
1218
+ #dontReport = [];
1219
+ #reported = /* @__PURE__ */ new WeakSet();
1220
+ reportUsing(callback) {
1221
+ this.#reportCallbacks.push(callback);
1222
+ }
1223
+ renderUsing(callback) {
1224
+ this.#renderCallbacks.push(callback);
1225
+ }
1226
+ ignore(...classes) {
1227
+ this.#dontReport.push(...classes);
1228
+ }
1229
+ shouldReport(error) {
1230
+ const kind = kindOf(error);
1231
+ if (kind !== void 0 && NOT_REPORTED.has(kind)) return false;
1232
+ return !this.#dontReport.some((cls) => error instanceof cls);
1233
+ }
1234
+ /** Report callbacks first (returning false stops the default log), then LogManager.error(), else console.error. Once per error. */
1235
+ async report(error) {
1236
+ if (!this.shouldReport(error)) return;
1237
+ if (typeof error === "object" && error !== null) {
1238
+ if (this.#reported.has(error)) return;
1239
+ this.#reported.add(error);
1240
+ }
1241
+ try {
1242
+ for (const callback of this.#reportCallbacks) if (await callback(error) === false) return;
1243
+ const container = this.#container();
1244
+ if (container.bound(LogManager)) {
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ container.make(LogManager).error(message, { exception: error });
1247
+ } else {
1248
+ console.error(error);
1249
+ }
1250
+ } catch (reportError) {
1251
+ console.error(reportError);
1252
+ }
1253
+ }
1254
+ async render(request2, error) {
1255
+ for (const callback of this.#renderCallbacks) {
1256
+ const response3 = await callback(error, request2);
1257
+ if (response3 instanceof Response) return response3;
1258
+ }
1259
+ if (error instanceof HttpResponseException) return error.response;
1260
+ const custom = await this.#renderCustom(request2, error);
1261
+ if (custom !== void 0) return custom;
1262
+ const http = this.#httpRequest(request2);
1263
+ const kind = kindOf(error);
1264
+ let response2;
1265
+ if (kind === "Validation") response2 = await this.#renderValidation(http, error);
1266
+ else if (kind === "Authentication") response2 = await this.#renderAuthentication(http, error);
1267
+ else if (http.expectsJson()) response2 = this.#renderJson(error);
1268
+ else response2 = await this.#renderHtml(http, error);
1269
+ const headers = error?.headers;
1270
+ if (headers !== void 0) for (const [name, value] of Object.entries(headers)) response2.headers.set(name, value);
1271
+ return response2;
1272
+ }
1273
+ /** An error with its own render(request) or toResponse(request) (Laravel's renderable exceptions / Responsable). */
1274
+ async #renderCustom(request2, error) {
1275
+ if (typeof error !== "object" || error === null) return void 0;
1276
+ const candidate = error;
1277
+ for (const method of [candidate.render, candidate.toResponse]) {
1278
+ if (typeof method !== "function") continue;
1279
+ const response2 = await method.call(error, request2);
1280
+ if (response2 instanceof Response) return response2;
1281
+ }
1282
+ return void 0;
1283
+ }
1284
+ async #renderValidation(request2, error) {
1285
+ if (request2.expectsJson()) {
1286
+ return this.#json({ message: summarizeValidation(error.errors), errors: error.errors }, statusFor(error));
1287
+ }
1288
+ const input = { ...request2.input() };
1289
+ for (const key of this.dontFlash) delete input[key];
1290
+ const container = this.#container();
1291
+ if (!container.bound(Redirector2)) {
1292
+ return this.#redirect(error.redirectTo ?? request2.header("referer") ?? "/");
1293
+ }
1294
+ const redirector = container.make(Redirector2);
1295
+ const redirect2 = error.redirectTo !== void 0 ? redirector.to(error.redirectTo) : redirector.back();
1296
+ const response2 = await redirect2.withInput(input).withErrors(error.errors, error.errorBag).toResponse(request2);
1297
+ return response2;
1298
+ }
1299
+ async #renderAuthentication(request2, error) {
1300
+ if (request2.expectsJson()) return this.#json({ message: error.message || "Unauthenticated." }, 401);
1301
+ const target = error.redirectTo ?? this.#guestRedirect(request2);
1302
+ const container = this.#container();
1303
+ if (!container.bound(Redirector2)) return this.#redirect(target);
1304
+ const redirector = container.make(Redirector2);
1305
+ const redirect2 = typeof redirector.guest === "function" ? redirector.guest(target) : redirector.to(target);
1306
+ return await redirect2.toResponse(request2);
1307
+ }
1308
+ /** config('http.redirect_guests_to'), then route('login'), then '/login'. */
1309
+ #guestRedirect(request2) {
1310
+ const configured = this.#config("http.redirect_guests_to", void 0);
1311
+ if (typeof configured === "function") return String(configured(request2));
1312
+ if (typeof configured === "string" && configured !== "") return configured;
1313
+ const container = this.#container();
1314
+ if (container.bound(UrlGenerator3)) {
1315
+ const url2 = container.make(UrlGenerator3);
1316
+ if (url2.hasRoute("login")) return url2.route("login");
1317
+ }
1318
+ return "/login";
1319
+ }
1320
+ #renderJson(error) {
1321
+ const status = statusFor(error);
1322
+ if (isHttpLike(error)) {
1323
+ const message = error instanceof Error && error.message !== "" ? error.message : statusText(status);
1324
+ return this.#json({ message }, status);
1325
+ }
1326
+ if (!this.#debug()) return this.#json({ message: "Server Error" }, status);
1327
+ const err = error instanceof Error ? error : new Error(String(error));
1328
+ const [frame] = stackFrames(err);
1329
+ return this.#json(
1330
+ {
1331
+ message: err.message,
1332
+ exception: err.name,
1333
+ file: frame?.file,
1334
+ line: frame?.line,
1335
+ trace: stackFrames(err).map((entry) => entry.text)
1336
+ },
1337
+ status
1338
+ );
1339
+ }
1340
+ async #renderHtml(request2, error) {
1341
+ const status = statusFor(error);
1342
+ if (!isHttpLike(error) && this.#debug()) {
1343
+ return this.#html(await debugPage(error, request2.method(), request2.fullUrl()), status);
1344
+ }
1345
+ const message = status === 403 && error instanceof Error && error.message !== "" ? error.message : statusText(status);
1346
+ const view = await this.#errorView(status, error, message);
1347
+ return this.#html(view ?? minimalPage(status, message), status);
1348
+ }
1349
+ /** resources/views/errors/{status}.tsx, then errors/4xx or errors/5xx, through the ViewFactory. */
1350
+ async #errorView(status, error, message) {
1351
+ const container = this.#container();
1352
+ if (!container.bound(ViewFactory2)) return void 0;
1353
+ const views = container.make(ViewFactory2);
1354
+ for (const name of [`errors.${status}`, `errors.${Math.floor(status / 100)}xx`]) {
1355
+ if (!views.exists(name)) continue;
1356
+ try {
1357
+ return await views.make(name, { exception: error, status, message }).render();
1358
+ } catch (viewError) {
1359
+ await this.report(viewError);
1360
+ return void 0;
1361
+ }
1362
+ }
1363
+ return void 0;
1364
+ }
1365
+ #httpRequest(request2) {
1366
+ if (typeof request2 === "object" && request2 !== null && typeof request2.expectsJson === "function") {
1367
+ return request2;
1368
+ }
1369
+ return new IncomingRequest(request2 instanceof Request ? request2 : new Request("http://localhost/"));
1370
+ }
1371
+ #json(body, status) {
1372
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1373
+ }
1374
+ #html(body, status) {
1375
+ return new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8" } });
1376
+ }
1377
+ #redirect(location) {
1378
+ return new Response(null, { status: 302, headers: { Location: location } });
1379
+ }
1380
+ #debug() {
1381
+ return this.#config("app.debug", false) === true;
1382
+ }
1383
+ #config(key, fallback) {
1384
+ return this.app.bound(ConfigRepository3) ? this.app.make(ConfigRepository3).get(key, fallback) : fallback;
1385
+ }
1386
+ /** The request scope while handling a request (so scoped services resolve), else the application. */
1387
+ #container() {
1388
+ return currentContainer4() ?? this.app;
1389
+ }
1390
+ };
1391
+
1392
+ // ExceptionsConfiguration.ts
1393
+ import { ExceptionCallbacks } from "@orieljs/contracts";
1394
+ var ExceptionsConfiguration = class {
1395
+ constructor(handler) {
1396
+ this.handler = handler;
1397
+ }
1398
+ handler;
1399
+ report(callback) {
1400
+ this.handler.reportUsing(callback);
1401
+ return this;
1402
+ }
1403
+ render(callback) {
1404
+ this.handler.renderUsing(callback);
1405
+ return this;
1406
+ }
1407
+ dontReport(...classes) {
1408
+ this.handler.ignore(...classes);
1409
+ return this;
1410
+ }
1411
+ };
1412
+ function applyExceptionCallbacks(app, handler) {
1413
+ if (!app.bound(ExceptionCallbacks)) return;
1414
+ const configurator = new ExceptionsConfiguration(handler);
1415
+ for (const callback of app.make(ExceptionCallbacks)) callback(configurator);
1416
+ }
1417
+
1418
+ // server/types.ts
1419
+ import { HttpKernel } from "@orieljs/contracts";
1420
+ async function terminateRequest(app, request2, response2) {
1421
+ try {
1422
+ if (app.bound(HttpKernel)) await app.make(HttpKernel).terminate?.(request2, response2);
1423
+ } catch (error) {
1424
+ console.error(error);
1425
+ }
1426
+ }
1427
+ async function handleSafely(app, request2) {
1428
+ try {
1429
+ return await app.handle(request2);
1430
+ } catch (error) {
1431
+ console.error(error);
1432
+ return plainServerError();
1433
+ }
1434
+ }
1435
+ function exitOnSignals(close, closed) {
1436
+ const onSignal = () => {
1437
+ void close().then(() => process.exit(0));
1438
+ };
1439
+ process.once("SIGINT", onSignal);
1440
+ process.once("SIGTERM", onSignal);
1441
+ void closed.then(() => {
1442
+ process.off("SIGINT", onSignal);
1443
+ process.off("SIGTERM", onSignal);
1444
+ });
1445
+ }
1446
+ var displayHost = (hostname) => hostname.includes(":") ? `[${hostname}]` : hostname;
1447
+
1448
+ // server/bun.ts
1449
+ function bunRuntime() {
1450
+ return globalThis.Bun;
1451
+ }
1452
+ async function serveBun(app, options) {
1453
+ const bun = bunRuntime();
1454
+ if (bun === void 0) throw new HttpServerError("Bun.serve is not available: the process is not running on Bun.");
1455
+ let markClosed = () => void 0;
1456
+ const closed = new Promise((resolve) => markClosed = resolve);
1457
+ const server = bun.serve({
1458
+ port: options.port,
1459
+ hostname: options.hostname,
1460
+ async fetch(request2, current) {
1461
+ setClientAddress(request2, current.requestIP(request2)?.address);
1462
+ const response2 = await handleSafely(app, request2);
1463
+ setTimeout(() => void terminateRequest(app, request2, response2), 0);
1464
+ return response2;
1465
+ },
1466
+ error(error) {
1467
+ console.error(error);
1468
+ return plainServerError();
1469
+ }
1470
+ });
1471
+ let closing;
1472
+ const close = () => closing ??= (async () => {
1473
+ let timer;
1474
+ const forced = new Promise((resolve) => {
1475
+ timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve), options.shutdownTimeout);
1476
+ });
1477
+ await Promise.race([Promise.resolve(server.stop(false)), forced]);
1478
+ clearTimeout(timer);
1479
+ markClosed();
1480
+ })();
1481
+ if (options.signals) exitOnSignals(close, closed);
1482
+ return {
1483
+ runtime: "bun",
1484
+ hostname: options.hostname,
1485
+ port: server.port,
1486
+ url: `http://${displayHost(options.hostname)}:${server.port}`,
1487
+ close,
1488
+ closed
1489
+ };
1490
+ }
1491
+
1492
+ // server/node.ts
1493
+ import { createServer } from "http";
1494
+ import { Readable as Readable2 } from "stream";
1495
+ import { pipeline } from "stream/promises";
1496
+ function requestBody(req) {
1497
+ let finished = false;
1498
+ const detach = () => {
1499
+ finished = true;
1500
+ req.off("data", onData);
1501
+ req.off("end", onEnd);
1502
+ req.off("error", onError);
1503
+ req.off("close", onClose);
1504
+ };
1505
+ let controller;
1506
+ const onData = (chunk) => {
1507
+ if (finished) return;
1508
+ controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
1509
+ if ((controller.desiredSize ?? 0) <= 0) req.pause();
1510
+ };
1511
+ const onEnd = () => {
1512
+ if (finished) return;
1513
+ detach();
1514
+ controller.close();
1515
+ };
1516
+ const onError = (error) => {
1517
+ if (finished) return;
1518
+ detach();
1519
+ controller.error(error);
1520
+ };
1521
+ const onClose = () => {
1522
+ if (!finished && !req.complete) onError(new Error("The request was aborted."));
1523
+ };
1524
+ return new ReadableStream(
1525
+ {
1526
+ start(c) {
1527
+ controller = c;
1528
+ req.on("data", onData);
1529
+ req.on("end", onEnd);
1530
+ req.on("error", onError);
1531
+ req.on("close", onClose);
1532
+ req.pause();
1533
+ },
1534
+ pull() {
1535
+ req.resume();
1536
+ },
1537
+ cancel() {
1538
+ detach();
1539
+ }
1540
+ },
1541
+ { highWaterMark: 0 }
1542
+ );
1543
+ }
1544
+ function toWebRequest(req, res, fallbackHost) {
1545
+ const target = req.url ?? "/";
1546
+ const url2 = new URL(`http://${req.headers.host ?? fallbackHost}${target.startsWith("/") ? target : `/${target}`}`);
1547
+ const headers = new Headers();
1548
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
1549
+ const name = req.rawHeaders[i];
1550
+ if (!name.startsWith(":")) headers.append(name, req.rawHeaders[i + 1]);
1551
+ }
1552
+ const method = (req.method ?? "GET").toUpperCase();
1553
+ const controller = new AbortController();
1554
+ res.once("close", () => {
1555
+ if (!res.writableFinished) controller.abort();
1556
+ });
1557
+ const init = { method, headers, signal: controller.signal };
1558
+ if (method !== "GET" && method !== "HEAD") {
1559
+ init.body = requestBody(req);
1560
+ init.duplex = "half";
1561
+ }
1562
+ return new Request(url2, init);
1563
+ }
1564
+ async function writeResponse(res, response2) {
1565
+ res.statusCode = response2.status;
1566
+ if (response2.statusText !== "") res.statusMessage = response2.statusText;
1567
+ response2.headers.forEach((value, name) => {
1568
+ if (name !== "set-cookie") res.setHeader(name, value);
1569
+ });
1570
+ const cookies = response2.headers.getSetCookie();
1571
+ if (cookies.length > 0) res.setHeader("set-cookie", cookies);
1572
+ if (response2.body === null) {
1573
+ res.end();
1574
+ return;
1575
+ }
1576
+ await pipeline(Readable2.fromWeb(response2.body), res);
1577
+ }
1578
+ async function serveNode(app, options) {
1579
+ const fallbackHost = `${displayHost(options.hostname)}:${options.port}`;
1580
+ const server = createServer((req, res) => {
1581
+ void (async () => {
1582
+ let request2;
1583
+ try {
1584
+ request2 = toWebRequest(req, res, fallbackHost);
1585
+ } catch {
1586
+ res.statusCode = 400;
1587
+ res.end("Bad Request");
1588
+ return;
1589
+ }
1590
+ setClientAddress(request2, req.socket.remoteAddress);
1591
+ const response2 = await handleSafely(app, request2);
1592
+ try {
1593
+ await writeResponse(res, response2);
1594
+ } catch {
1595
+ res.destroy();
1596
+ }
1597
+ await terminateRequest(app, request2, response2);
1598
+ })();
1599
+ });
1600
+ await new Promise((resolve, reject) => {
1601
+ server.once("error", reject);
1602
+ server.listen(options.port, options.hostname, () => {
1603
+ server.off("error", reject);
1604
+ resolve();
1605
+ });
1606
+ });
1607
+ const address = server.address();
1608
+ const closed = new Promise((resolve) => server.once("close", () => resolve()));
1609
+ let closing;
1610
+ const close = () => closing ??= (async () => {
1611
+ const done = new Promise((resolve) => server.close(() => resolve()));
1612
+ server.closeIdleConnections();
1613
+ const idle = setInterval(() => server.closeIdleConnections(), 50);
1614
+ const force = setTimeout(() => server.closeAllConnections(), options.shutdownTimeout);
1615
+ await done;
1616
+ clearInterval(idle);
1617
+ clearTimeout(force);
1618
+ })();
1619
+ if (options.signals) exitOnSignals(close, closed);
1620
+ return {
1621
+ runtime: "node",
1622
+ hostname: options.hostname,
1623
+ port: address.port,
1624
+ url: `http://${displayHost(options.hostname)}:${address.port}`,
1625
+ close,
1626
+ closed
1627
+ };
1628
+ }
1629
+
1630
+ // server/serve.ts
1631
+ function detectRuntime() {
1632
+ return bunRuntime() !== void 0 ? "bun" : "node";
1633
+ }
1634
+ async function serve(app, options = {}) {
1635
+ const resolved = {
1636
+ port: options.port ?? Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3),
1637
+ hostname: options.hostname ?? "127.0.0.1",
1638
+ signals: options.signals ?? true,
1639
+ shutdownTimeout: options.shutdownTimeout ?? 1e4
1640
+ };
1641
+ await app.boot();
1642
+ const runtime = options.runtime ?? detectRuntime();
1643
+ const server = runtime === "bun" ? await serveBun(app, resolved) : await serveNode(app, resolved);
1644
+ const onListen = options.onListen ?? ((running) => console.log(`Server running on [${running.url}].`));
1645
+ onListen(server);
1646
+ return server;
1647
+ }
1648
+
1649
+ // commands.ts
1650
+ import { existsSync } from "fs";
1651
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
1652
+ import { dirname } from "path";
1653
+ import {
1654
+ Router as Router3
1655
+ } from "@orieljs/contracts";
1656
+ function io(command) {
1657
+ return {
1658
+ option: (name) => {
1659
+ const value = command.option(name);
1660
+ return value === void 0 || value === null || value === false || value === "" ? void 0 : String(value);
1661
+ },
1662
+ flag: (name) => {
1663
+ const value = command.option(name);
1664
+ return value === true || value === "true" || value === "1";
1665
+ },
1666
+ info: (message) => command.info(message),
1667
+ line: (message) => command.line(message),
1668
+ warn: (message) => command.warn(message),
1669
+ error: (message) => command.error(message)
1670
+ };
1671
+ }
1672
+ var middlewareName = (reference) => typeof reference === "string" ? reference : reference.name;
1673
+ function routeListRows(routes) {
1674
+ const rows = routes.map((route2) => {
1675
+ const listable = route2;
1676
+ return {
1677
+ method: route2.methods.join("|"),
1678
+ uri: route2.uri,
1679
+ name: route2.getName() ?? null,
1680
+ action: typeof listable.getActionName === "function" ? listable.getActionName() : "Closure",
1681
+ middleware: typeof listable.gatherMiddleware === "function" ? listable.gatherMiddleware().map(middlewareName) : []
1682
+ };
1683
+ });
1684
+ return rows.sort((a, b) => a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0);
1685
+ }
1686
+ function filterRouteList(rows, filters) {
1687
+ const excluded = (filters.exceptPath ?? "").split(",").filter((path) => path !== "");
1688
+ return rows.filter((row) => {
1689
+ if (filters.name !== void 0 && !(row.name ?? "").includes(filters.name)) return false;
1690
+ if (filters.path !== void 0 && !row.uri.includes(filters.path)) return false;
1691
+ if (filters.method !== void 0 && !row.method.includes(filters.method.toUpperCase())) return false;
1692
+ return !excluded.some((path) => row.uri.includes(path));
1693
+ });
1694
+ }
1695
+ function formatRouteList(rows, width) {
1696
+ const methodWidth = Math.max(...rows.map((row) => row.method.length));
1697
+ const lines = rows.map((row) => {
1698
+ const spaces = " ".repeat(Math.max(methodWidth + 6 - row.method.length, 0));
1699
+ const action = row.action === "Closure" ? row.name ?? "" : row.name === null ? row.action : `${row.name} \u203A ${row.action}`;
1700
+ const used = row.method.length + spaces.length + row.uri.length + action.length + 6 + (action === "" ? 0 : 1);
1701
+ const dots = ".".repeat(Math.max(width - used, 0));
1702
+ return ` ${row.method} ${spaces}${row.uri}${dots === "" ? "" : ` ${dots}`}${action === "" ? "" : ` ${action}`}`;
1703
+ });
1704
+ const count = `Showing [${rows.length}] ${rows.length === 1 ? "route" : "routes"}`;
1705
+ return [...lines, "", `${" ".repeat(Math.max(width - count.length - 2, 0))}${count}`];
1706
+ }
1707
+ var API_ROUTES_STUB = `import { request, Route } from '@orieljs/framework'
1708
+
1709
+ Route.get('/user', () => request().user()).middleware('auth:token')
1710
+ `;
1711
+ function addApiRouting(source) {
1712
+ const code = source.replace(/^[ \t]*\/\/.*$/gm, "");
1713
+ if (/withRouting\(\s*\{[^}]*\bapi\s*:/.test(code)) return source;
1714
+ const commented = /^([ \t]*)\/\/\s*(api:\s*['"`][^'"`\n]*['"`],?)[ \t]*$/m.exec(source);
1715
+ if (commented !== null) return source.replace(commented[0], () => `${commented[1]}${commented[2]}`);
1716
+ const web = /^([ \t]*)web:\s*(['"`])[^'"`\n]*\2[ \t]*,?[ \t]*$/m.exec(source);
1717
+ if (web !== null) {
1718
+ const line = web[0].trimEnd();
1719
+ return source.replace(web[0], () => `${line.endsWith(",") ? line : `${line},`}
1720
+ ${web[1]}api: 'routes/api.ts',`);
1721
+ }
1722
+ const inline = /withRouting\(\s*\{/.exec(source);
1723
+ if (inline !== null) return source.replace(inline[0], () => `${inline[0]} api: 'routes/api.ts',`);
1724
+ const create = /^([ \t]*)\.create\(\)/m.exec(source);
1725
+ if (create !== null) return source.replace(create[0], () => `${create[1]}.withRouting({ api: 'routes/api.ts' })
1726
+ ${create[0]}`);
1727
+ return void 0;
1728
+ }
1729
+ function registerHttpCommands(kernel, app, options = {}) {
1730
+ const serveApp = options.serve ?? serve;
1731
+ const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
1732
+ kernel.command(
1733
+ "serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on}",
1734
+ async function() {
1735
+ const out = io(this);
1736
+ const port = out.option("port");
1737
+ if (port !== void 0 && !/^\d+$/.test(port)) {
1738
+ out.error("The --port option must be a number.");
1739
+ return 1;
1740
+ }
1741
+ const server = await serveApp(app, {
1742
+ hostname: out.option("host") ?? "127.0.0.1",
1743
+ ...port !== void 0 && { port: Number(port) },
1744
+ onListen: (running) => {
1745
+ out.info(`Server running on [${running.url}].`);
1746
+ out.line("Press Ctrl+C to stop the server");
1747
+ }
1748
+ });
1749
+ await server.closed;
1750
+ return 0;
1751
+ }
1752
+ ).purpose("Serve the application on the Oriel development server");
1753
+ kernel.command(
1754
+ "route:list {--method= : Filter the routes by method} {--name= : Filter the routes by name} {--path= : Only show routes matching the given path pattern} {--except-path= : Do not display the routes matching the given path pattern} {--reverse : Reverse the ordering of the routes} {--json : Output the route list as JSON}",
1755
+ function() {
1756
+ const out = io(this);
1757
+ if (!app.bound(Router3)) {
1758
+ out.error("The router is not available. Is the Routing service provider registered?");
1759
+ return 1;
1760
+ }
1761
+ const all = routeListRows(app.make(Router3).getRoutes());
1762
+ if (all.length === 0) {
1763
+ out.error("Your application doesn't have any routes.");
1764
+ return 0;
1765
+ }
1766
+ const rows = filterRouteList(all, {
1767
+ ...out.option("method") !== void 0 && { method: out.option("method") },
1768
+ ...out.option("name") !== void 0 && { name: out.option("name") },
1769
+ ...out.option("path") !== void 0 && { path: out.option("path") },
1770
+ ...out.option("except-path") !== void 0 && { exceptPath: out.option("except-path") }
1771
+ });
1772
+ if (rows.length === 0) {
1773
+ out.error("Your application doesn't have any routes matching the given criteria.");
1774
+ return 0;
1775
+ }
1776
+ if (out.flag("reverse")) rows.reverse();
1777
+ if (out.flag("json")) {
1778
+ out.line(JSON.stringify(rows));
1779
+ return 0;
1780
+ }
1781
+ for (const line of formatRouteList(rows, terminalWidth())) out.line(line);
1782
+ return 0;
1783
+ }
1784
+ ).purpose("List all registered routes");
1785
+ kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
1786
+ const out = io(this);
1787
+ const routesFile = app.basePath("routes/api.ts");
1788
+ if (existsSync(routesFile) && !out.flag("force")) {
1789
+ out.error("API routes file already exists.");
1790
+ } else {
1791
+ await mkdir(dirname(routesFile), { recursive: true });
1792
+ await writeFile(routesFile, API_ROUTES_STUB);
1793
+ out.info("Published API routes file.");
1794
+ }
1795
+ const bootstrapFile = app.bootstrapPath("app.ts");
1796
+ const source = existsSync(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
1797
+ const updated = source === void 0 ? void 0 : addApiRouting(source);
1798
+ if (updated === void 0) {
1799
+ out.warn(
1800
+ "Unable to automatically add API route definition to bootstrap file. Add api: 'routes/api.ts' to withRouting() in bootstrap/app.ts."
1801
+ );
1802
+ } else if (updated !== source) {
1803
+ await writeFile(bootstrapFile, updated);
1804
+ }
1805
+ out.info("API scaffolding installed. Run [oriel migrate] to create the personal access tokens table.");
1806
+ return 0;
1807
+ }).purpose("Create an API routes file and prepare the application for API authentication");
1808
+ }
1809
+
1810
+ // HttpServiceProvider.ts
1811
+ import { ConsoleKernel, ExceptionHandler as ExceptionHandler2, HttpKernel as HttpKernel2, Redirector as Redirector3, ResponseFactory as ResponseFactory2, Router as Router4 } from "@orieljs/contracts";
1812
+ import { ServiceProvider } from "@orieljs/support";
1813
+ var HttpServiceProvider = class extends ServiceProvider {
1814
+ register() {
1815
+ this.mergeConfigFrom(
1816
+ {
1817
+ trusted_proxies: [],
1818
+ max_body_size: 8 * 1024 * 1024,
1819
+ csrf_except: [],
1820
+ redirect_guests_to: void 0,
1821
+ redirect_users_to: void 0
1822
+ },
1823
+ "http"
1824
+ );
1825
+ this.app.singleton(HttpKernel2, () => new Kernel(this.app));
1826
+ this.app.singleton(ExceptionHandler2, () => {
1827
+ const handler = new ErrorHandler(this.app);
1828
+ applyExceptionCallbacks(this.app, handler);
1829
+ return handler;
1830
+ });
1831
+ this.app.singleton(ResponseFactory2, () => new HttpResponseFactory());
1832
+ this.app.singleton(Redirector3, () => new HttpRedirector());
1833
+ }
1834
+ boot() {
1835
+ this.app.booted(() => {
1836
+ const kernel = this.app.make(HttpKernel2);
1837
+ if (kernel instanceof Kernel && this.app.bound(Router4)) applyMiddlewareCallbacks(this.app, kernel);
1838
+ });
1839
+ if (this.app.bound(ConsoleKernel)) registerHttpCommands(this.app.make(ConsoleKernel), this.app);
1840
+ }
1841
+ };
1842
+ export {
1843
+ API_ROUTES_STUB,
1844
+ ConvertEmptyStringsToNull,
1845
+ ErrorHandler,
1846
+ ExceptionsConfiguration,
1847
+ FormUploadedFile,
1848
+ HttpException,
1849
+ HttpRedirectResponse,
1850
+ HttpRedirector,
1851
+ HttpResponseException,
1852
+ HttpResponseFactory,
1853
+ HttpServerError,
1854
+ HttpServiceProvider,
1855
+ IncomingRequest,
1856
+ Kernel,
1857
+ MiddlewareConfiguration,
1858
+ TrimStrings,
1859
+ abort,
1860
+ abortIf,
1861
+ abortUnless,
1862
+ addApiRouting,
1863
+ applyExceptionCallbacks,
1864
+ applyMiddlewareCallbacks,
1865
+ back,
1866
+ bunRuntime,
1867
+ clientAddress,
1868
+ contentDisposition,
1869
+ currentRequest,
1870
+ debugPage,
1871
+ detectRuntime,
1872
+ escapeHtml,
1873
+ extensionFromMime,
1874
+ filterRouteList,
1875
+ formatRouteList,
1876
+ fromEntries,
1877
+ ipMatches,
1878
+ isBlank,
1879
+ mergeRecursive,
1880
+ mimeFromExtension,
1881
+ minimalPage,
1882
+ normalizeLists,
1883
+ optionalCookieJar,
1884
+ optionalSession,
1885
+ parseBracketKey,
1886
+ readBody,
1887
+ redirect,
1888
+ registerHttpCommands,
1889
+ request,
1890
+ requestScope,
1891
+ response,
1892
+ route,
1893
+ routeListRows,
1894
+ sanitize,
1895
+ serve,
1896
+ serveBun,
1897
+ serveNode,
1898
+ setClientAddress,
1899
+ stackFrames,
1900
+ statusFor,
1901
+ statusText,
1902
+ summarizeValidation,
1903
+ toWebRequest,
1904
+ transformLeaves,
1905
+ url,
1906
+ writeResponse
1907
+ };
1908
+ //# sourceMappingURL=index.js.map