@orieljs/http 0.1.0-alpha.1 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28,6 +28,21 @@ var HttpServerError = class extends Error {
28
28
  };
29
29
 
30
30
  // body.ts
31
+ function exceedsJsonDepth(text, maxDepth) {
32
+ let depth = 0;
33
+ let inString = false;
34
+ for (let i = 0; i < text.length; i++) {
35
+ const code = text.charCodeAt(i);
36
+ if (inString) {
37
+ if (code === 92) i++;
38
+ else if (code === 34) inString = false;
39
+ } else if (code === 34) inString = true;
40
+ else if (code === 123 || code === 91) {
41
+ if (++depth > maxDepth) return true;
42
+ } else if (code === 125 || code === 93) depth--;
43
+ }
44
+ return false;
45
+ }
31
46
  async function readBody(raw, limit) {
32
47
  if (raw.body === null) return new Uint8Array(0);
33
48
  const declared = Number(raw.headers.get("content-length") ?? NaN);
@@ -146,6 +161,15 @@ var HttpRedirectResponse = class {
146
161
  // HttpRedirector.ts
147
162
  import { UrlGenerator } from "@orieljs/contracts";
148
163
  import { inject } from "@orieljs/container";
164
+ function pathOf(url2) {
165
+ let parsed;
166
+ try {
167
+ parsed = new URL(url2, "http://localhost");
168
+ } catch {
169
+ return "/";
170
+ }
171
+ return `${parsed.pathname.replace(/^\/+/, "/")}${parsed.search}${parsed.hash}`;
172
+ }
149
173
  var HttpRedirector = class {
150
174
  to(path, status = 302, headers) {
151
175
  return new HttpRedirectResponse(inject(UrlGenerator).to(path), status, headers);
@@ -164,13 +188,16 @@ var HttpRedirector = class {
164
188
  away(url2, status = 302) {
165
189
  return new HttpRedirectResponse(url2, status);
166
190
  }
167
- /** Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. */
191
+ /**
192
+ * Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. Only the path
193
+ * is stored, so intended() goes back on whichever host serves the app, and never to another site's Referer.
194
+ */
168
195
  guest(path, status = 302) {
169
196
  const session = optionalSession();
170
197
  const request2 = currentRequest();
171
198
  if (session !== void 0) {
172
199
  const intended = request2 !== void 0 && request2.method() === "GET" && !request2.expectsJson() ? request2.fullUrl() : inject(UrlGenerator).previous();
173
- session.put("url.intended", intended);
200
+ session.put("url.intended", pathOf(intended));
174
201
  }
175
202
  return this.to(path, status);
176
203
  }
@@ -195,7 +222,7 @@ function clientAddress(request2) {
195
222
  // FormUploadedFile.ts
196
223
  import { FilesystemManager } from "@orieljs/contracts";
197
224
  import { inject as inject2 } from "@orieljs/container";
198
- import { Str } from "@orieljs/support";
225
+ import { FileType, Str } from "@orieljs/support";
199
226
 
200
227
  // mime.ts
201
228
  var TYPES = {
@@ -210,13 +237,16 @@ var TYPES = {
210
237
  ico: "image/vnd.microsoft.icon",
211
238
  jpg: "image/jpeg",
212
239
  js: "text/javascript",
240
+ map: "application/json",
213
241
  json: "application/json",
214
242
  md: "text/markdown",
215
243
  mp3: "audio/mpeg",
216
244
  mp4: "video/mp4",
245
+ otf: "font/otf",
217
246
  pdf: "application/pdf",
218
247
  png: "image/png",
219
248
  svg: "image/svg+xml",
249
+ ttf: "font/ttf",
220
250
  txt: "text/plain",
221
251
  wasm: "application/wasm",
222
252
  webm: "video/webm",
@@ -239,20 +269,60 @@ function extensionFromMime(type) {
239
269
  }
240
270
 
241
271
  // FormUploadedFile.ts
242
- var FormUploadedFile = class {
243
- constructor(file) {
272
+ var NEVER_CLAIMED = /* @__PURE__ */ new Set([
273
+ ...FileType.BINARY_AND_MARKUP_TYPES.flatMap(({ extensions }) => extensions),
274
+ "jpe",
275
+ "svgz",
276
+ "xhtml",
277
+ "xht",
278
+ "shtml",
279
+ "xsl",
280
+ "xslt",
281
+ "mht",
282
+ "mhtml",
283
+ "js",
284
+ "mjs",
285
+ "rss",
286
+ "atom",
287
+ "rdf",
288
+ "mathml",
289
+ "mml",
290
+ "xul"
291
+ ]);
292
+ var PLAIN_EXTENSION = /^[a-z0-9]+$/;
293
+ var FormUploadedFile = class _FormUploadedFile {
294
+ constructor(file, detected) {
244
295
  this.file = file;
296
+ this.detected = detected;
245
297
  this.clientName = file.name ?? "";
246
298
  this.mimeType = file.type === "" ? "application/octet-stream" : file.type;
247
299
  this.size = file.size;
248
300
  }
249
301
  file;
302
+ detected;
250
303
  clientName;
251
304
  mimeType;
252
305
  size;
253
- /** Guessed from the MIME type, falling back to the client's extension (Laravel's extension()). */
306
+ /**
307
+ * Reads the leading bytes first, so extension(), hashName() and store() name the file by its content. The request
308
+ * builds its uploads this way.
309
+ */
310
+ static async fromFile(file) {
311
+ return new _FormUploadedFile(file, FileType.detectFileType(new Uint8Array(await file.slice(0, FileType.SNIFF_BYTES).arrayBuffer())));
312
+ }
313
+ /**
314
+ * Laravel's extension(): the type the bytes show (guessExtension()), when they were read and match a known format.
315
+ * Text and bytes no format matches keep the client's claim (its MIME type, else its file name) when it is a plain
316
+ * extension, unless it names a format the bytes would have shown or one a browser renders or runs: otherwise 'txt'
317
+ * for text, '' for the rest. Unread (built directly), the client's claim when it is a plain extension, else ''.
318
+ */
254
319
  extension() {
255
- return extensionFromMime(this.mimeType) ?? this.clientExtension();
320
+ const claimed = extensionFromMime(this.mimeType) ?? this.clientExtension();
321
+ const detected = this.detected;
322
+ if (detected === void 0) return PLAIN_EXTENSION.test(claimed) ? claimed : "";
323
+ if (detected !== FileType.TEXT && detected.extensions.length > 0) return detected.extensions[0];
324
+ if (PLAIN_EXTENSION.test(claimed) && !NEVER_CLAIMED.has(claimed)) return claimed;
325
+ return detected === FileType.TEXT ? "txt" : "";
256
326
  }
257
327
  clientExtension() {
258
328
  const dot = this.clientName.lastIndexOf(".");
@@ -282,6 +352,7 @@ var FormUploadedFile = class {
282
352
 
283
353
  // IncomingRequest.ts
284
354
  import {
355
+ AuthFactory,
285
356
  CurrentRoute,
286
357
  Session as Session2,
287
358
  ValidationFactory
@@ -404,6 +475,7 @@ var IncomingRequest = class {
404
475
  this.#url = new URL(raw.url);
405
476
  this.#trustedProxies = options.trustedProxies ?? [];
406
477
  this.#maxBodySize = options.maxBodySize === void 0 ? 8 * 1024 * 1024 : options.maxBodySize;
478
+ this.#maxJsonDepth = options.maxJsonDepth === void 0 ? 512 : options.maxJsonDepth;
407
479
  this.#query = fromEntries(this.#url.searchParams.entries());
408
480
  this.#method = raw.method.toUpperCase();
409
481
  }
@@ -411,6 +483,7 @@ var IncomingRequest = class {
411
483
  #url;
412
484
  #trustedProxies;
413
485
  #maxBodySize;
486
+ #maxJsonDepth;
414
487
  #query;
415
488
  #body = {};
416
489
  #files = {};
@@ -432,6 +505,9 @@ var IncomingRequest = class {
432
505
  const isMultipart = type.startsWith("multipart/form-data");
433
506
  if (isJson || isUrlencoded || isMultipart) {
434
507
  const bytes = await readBody(this.raw, this.#maxBodySize);
508
+ if (isJson && this.#maxJsonDepth !== null && exceedsJsonDepth(new TextDecoder().decode(bytes), this.#maxJsonDepth)) {
509
+ throw new HttpException(400, "The request body is nested too deeply.");
510
+ }
435
511
  try {
436
512
  if (isJson) {
437
513
  this.#content = new TextDecoder().decode(bytes);
@@ -440,7 +516,7 @@ var IncomingRequest = class {
440
516
  this.#content = new TextDecoder().decode(bytes);
441
517
  this.#body = fromEntries(new URLSearchParams(this.#content).entries());
442
518
  } else {
443
- this.#parseForm(await new Response(bytes, { headers: { "content-type": this.raw.headers.get("content-type") ?? "" } }).formData());
519
+ await this.#parseForm(await new Response(bytes, { headers: { "content-type": this.raw.headers.get("content-type") ?? "" } }).formData());
444
520
  }
445
521
  } catch {
446
522
  }
@@ -465,12 +541,12 @@ var IncomingRequest = class {
465
541
  if (Array.isArray(parsed)) return sanitize({ ...parsed });
466
542
  return isPlainObject(parsed) ? sanitize(parsed) : {};
467
543
  }
468
- #parseForm(form) {
544
+ async #parseForm(form) {
469
545
  const fields = [];
470
546
  const files = [];
471
547
  for (const [name, value] of form.entries()) {
472
548
  if (typeof value === "string") fields.push([name, value]);
473
- else if (!((value.name ?? "") === "" && value.size === 0)) files.push([name, new FormUploadedFile(value)]);
549
+ else if (!((value.name ?? "") === "" && value.size === 0)) files.push([name, await FormUploadedFile.fromFile(value)]);
474
550
  }
475
551
  this.#body = fromEntries(fields);
476
552
  this.#files = fromEntries(files);
@@ -610,9 +686,14 @@ var IncomingRequest = class {
610
686
  filled(key) {
611
687
  return !isBlank(Arr.get(this.all(), key));
612
688
  }
689
+ /** Laravel's string(): the input as a Stringable (the default when it is missing or null): request().string('q').trim(). */
613
690
  string(key, defaultValue = "") {
614
691
  const value = this.input(key);
615
- return value === void 0 || value === null ? defaultValue : String(value);
692
+ return Str2.of(value === void 0 || value === null ? defaultValue : value);
693
+ }
694
+ /** Laravel's str(): string() under its short name. */
695
+ str(key, defaultValue = "") {
696
+ return this.string(key, defaultValue);
616
697
  }
617
698
  /** PHP's FILTER_VALIDATE_BOOLEAN: '1', 'true', 'on', 'yes' (any case) and true are true. */
618
699
  boolean(key, defaultValue = false) {
@@ -688,13 +769,23 @@ var IncomingRequest = class {
688
769
  session() {
689
770
  return inject3(Session2);
690
771
  }
691
- user(guard) {
692
- return this.#userResolver?.(guard);
772
+ /**
773
+ * The signed-in user, through the resolver Auth sets, else the current scope's guard (the default, or `guard`).
774
+ * The guard keeps the user it found, so it is looked up once per request. Undefined for a guest or without auth.
775
+ */
776
+ async user(guard) {
777
+ if (this.#userResolver !== void 0) return await this.#userResolver(guard);
778
+ const container = currentContainer2();
779
+ if (container === void 0 || !container.bound(AuthFactory)) return void 0;
780
+ return await container.make(AuthFactory).guard(guard).user();
693
781
  }
694
782
  setUserResolver(resolver) {
695
783
  this.#userResolver = resolver;
696
784
  }
697
- /** Validates all() through the ValidationFactory; throws its Validation error on failure. */
785
+ /**
786
+ * Validates all() through the ValidationFactory; throws its Validation error on failure. The data is typed from the
787
+ * rules: `validate({ title: 'required|string' })` is `{ title: string }`.
788
+ */
698
789
  validate(rules, messages, attributes) {
699
790
  return inject3(ValidationFactory).make(this.all(), rules, messages, attributes).validated();
700
791
  }
@@ -738,7 +829,8 @@ function url(path, query, absolute) {
738
829
  const generator = inject4(UrlGenerator2);
739
830
  return path === void 0 ? generator : generator.to(path, query, absolute);
740
831
  }
741
- function route(name, parameters, absolute) {
832
+ function route(name, ...args) {
833
+ const [parameters, absolute] = args;
742
834
  return inject4(UrlGenerator2).route(name, parameters, absolute);
743
835
  }
744
836
  function abort(status, message = "", headers = {}) {
@@ -878,7 +970,11 @@ var Kernel = class {
878
970
  let request2;
879
971
  let response2;
880
972
  try {
881
- request2 = new IncomingRequest(raw, { trustedProxies: this.#trustedProxies(), maxBodySize: this.#maxBodySize() });
973
+ request2 = new IncomingRequest(raw, {
974
+ trustedProxies: this.#trustedProxies(),
975
+ maxBodySize: this.#maxBodySize(),
976
+ maxJsonDepth: this.#maxJsonDepth()
977
+ });
882
978
  scope.instance(HttpRequest3, request2);
883
979
  const state = { scope, request: request2, middleware: [] };
884
980
  this.#requests.set(raw, state);
@@ -990,6 +1086,16 @@ var Kernel = class {
990
1086
  `Invalid http.max_body_size ${JSON.stringify(value) ?? String(value)}: use a number of bytes (0 or more), or null for no limit.`
991
1087
  );
992
1088
  }
1089
+ /** config('http.max_json_depth'): how deep a JSON body may nest, or null for no limit. */
1090
+ #maxJsonDepth() {
1091
+ if (!this.app.bound(ConfigRepository)) return 512;
1092
+ const value = this.app.make(ConfigRepository).get("http.max_json_depth", 512);
1093
+ if (value === null) return null;
1094
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
1095
+ throw new HttpServerError(
1096
+ `Invalid http.max_json_depth ${JSON.stringify(value) ?? String(value)}: use a whole number (1 or more), or null for no limit.`
1097
+ );
1098
+ }
993
1099
  /** config('http.trusted_proxies'): an array, or a comma-separated string such as '*' or '10.0.0.0/8,::1'. */
994
1100
  #trustedProxies() {
995
1101
  if (!this.app.bound(ConfigRepository)) return [];
@@ -1089,18 +1195,41 @@ var STATUS_TEXTS = {
1089
1195
  403: "Forbidden",
1090
1196
  404: "Not Found",
1091
1197
  405: "Method Not Allowed",
1198
+ 406: "Not Acceptable",
1199
+ 407: "Proxy Authentication Required",
1092
1200
  408: "Request Timeout",
1093
1201
  409: "Conflict",
1094
1202
  410: "Gone",
1203
+ 411: "Length Required",
1204
+ 412: "Precondition Failed",
1095
1205
  413: "Content Too Large",
1206
+ 414: "URI Too Long",
1096
1207
  415: "Unsupported Media Type",
1208
+ 416: "Range Not Satisfiable",
1209
+ 417: "Expectation Failed",
1210
+ 418: "I'm a teapot",
1097
1211
  419: "Page Expired",
1212
+ 421: "Misdirected Request",
1098
1213
  422: "Unprocessable Content",
1214
+ 423: "Locked",
1215
+ 424: "Failed Dependency",
1216
+ 425: "Too Early",
1217
+ 426: "Upgrade Required",
1218
+ 428: "Precondition Required",
1099
1219
  429: "Too Many Requests",
1220
+ 431: "Request Header Fields Too Large",
1221
+ 451: "Unavailable For Legal Reasons",
1100
1222
  500: "Server Error",
1223
+ 501: "Not Implemented",
1101
1224
  502: "Bad Gateway",
1102
1225
  503: "Service Unavailable",
1103
- 504: "Gateway Timeout"
1226
+ 504: "Gateway Timeout",
1227
+ 505: "HTTP Version Not Supported",
1228
+ 506: "Variant Also Negotiates",
1229
+ 507: "Insufficient Storage",
1230
+ 508: "Loop Detected",
1231
+ 510: "Not Extended",
1232
+ 511: "Network Authentication Required"
1104
1233
  };
1105
1234
  function statusText(status) {
1106
1235
  return STATUS_TEXTS[status] ?? (status >= 500 ? "Server Error" : "Error");
@@ -1217,6 +1346,7 @@ var ErrorHandler = class {
1217
1346
  #renderCallbacks = [];
1218
1347
  #dontReport = [];
1219
1348
  #reported = /* @__PURE__ */ new WeakSet();
1349
+ #jsonWhen;
1220
1350
  reportUsing(callback) {
1221
1351
  this.#reportCallbacks.push(callback);
1222
1352
  }
@@ -1226,6 +1356,14 @@ var ErrorHandler = class {
1226
1356
  ignore(...classes) {
1227
1357
  this.#dontReport.push(...classes);
1228
1358
  }
1359
+ /** Laravel's shouldRenderJsonWhen(): the callback decides JSON or HTML instead of the Accept header. */
1360
+ shouldRenderJsonWhen(callback) {
1361
+ this.#jsonWhen = callback;
1362
+ }
1363
+ /** Laravel's shouldReturnJson(): the shouldRenderJsonWhen() callback when set, else request.expectsJson(). */
1364
+ shouldReturnJson(request2, error) {
1365
+ return this.#jsonWhen !== void 0 ? this.#jsonWhen(request2, error) : request2.expectsJson();
1366
+ }
1229
1367
  shouldReport(error) {
1230
1368
  const kind = kindOf(error);
1231
1369
  if (kind !== void 0 && NOT_REPORTED.has(kind)) return false;
@@ -1264,11 +1402,10 @@ var ErrorHandler = class {
1264
1402
  let response2;
1265
1403
  if (kind === "Validation") response2 = await this.#renderValidation(http, error);
1266
1404
  else if (kind === "Authentication") response2 = await this.#renderAuthentication(http, error);
1267
- else if (http.expectsJson()) response2 = this.#renderJson(error);
1405
+ else if (this.shouldReturnJson(http, error)) response2 = this.#renderJson(error);
1268
1406
  else response2 = await this.#renderHtml(http, error);
1269
1407
  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;
1408
+ return headers === void 0 ? response2 : withHeaders(response2, headers);
1272
1409
  }
1273
1410
  /** An error with its own render(request) or toResponse(request) (Laravel's renderable exceptions / Responsable). */
1274
1411
  async #renderCustom(request2, error) {
@@ -1282,7 +1419,7 @@ var ErrorHandler = class {
1282
1419
  return void 0;
1283
1420
  }
1284
1421
  async #renderValidation(request2, error) {
1285
- if (request2.expectsJson()) {
1422
+ if (this.shouldReturnJson(request2, error)) {
1286
1423
  return this.#json({ message: summarizeValidation(error.errors), errors: error.errors }, statusFor(error));
1287
1424
  }
1288
1425
  const input = { ...request2.input() };
@@ -1297,7 +1434,7 @@ var ErrorHandler = class {
1297
1434
  return response2;
1298
1435
  }
1299
1436
  async #renderAuthentication(request2, error) {
1300
- if (request2.expectsJson()) return this.#json({ message: error.message || "Unauthenticated." }, 401);
1437
+ if (this.shouldReturnJson(request2, error)) return this.#json({ message: error.message || "Unauthenticated." }, 401);
1301
1438
  const target = error.redirectTo ?? this.#guestRedirect(request2);
1302
1439
  const container = this.#container();
1303
1440
  if (!container.bound(Redirector2)) return this.#redirect(target);
@@ -1388,6 +1525,11 @@ var ErrorHandler = class {
1388
1525
  return currentContainer4() ?? this.app;
1389
1526
  }
1390
1527
  };
1528
+ function withHeaders(response2, extra) {
1529
+ const headers = new Headers(response2.headers);
1530
+ for (const [name, value] of Object.entries(extra)) headers.set(name, value);
1531
+ return new Response(response2.body, { status: response2.status, statusText: response2.statusText, headers });
1532
+ }
1391
1533
 
1392
1534
  // ExceptionsConfiguration.ts
1393
1535
  import { ExceptionCallbacks } from "@orieljs/contracts";
@@ -1408,6 +1550,11 @@ var ExceptionsConfiguration = class {
1408
1550
  this.handler.ignore(...classes);
1409
1551
  return this;
1410
1552
  }
1553
+ /** shouldRenderJsonWhen((request) => request.is('api/*')): JSON errors for those requests, whatever they accept. */
1554
+ shouldRenderJsonWhen(callback) {
1555
+ this.handler.shouldRenderJsonWhen(callback);
1556
+ return this;
1557
+ }
1411
1558
  };
1412
1559
  function applyExceptionCallbacks(app, handler) {
1413
1560
  if (!app.bound(ExceptionCallbacks)) return;
@@ -1417,6 +1564,41 @@ function applyExceptionCallbacks(app, handler) {
1417
1564
 
1418
1565
  // server/types.ts
1419
1566
  import { HttpKernel } from "@orieljs/contracts";
1567
+
1568
+ // server/static.ts
1569
+ import { createReadStream as createReadStream2 } from "fs";
1570
+ import { stat as stat2 } from "fs/promises";
1571
+ import { Readable as Readable2 } from "stream";
1572
+ import { relative, resolve, sep } from "path";
1573
+ var HIDDEN_SEGMENT = /^\.|\.(ts|tsx|mts|cts)$|[. ]$/i;
1574
+ async function servePublicFile(publicPath, request2) {
1575
+ if (request2.method !== "GET" && request2.method !== "HEAD") return void 0;
1576
+ let pathname;
1577
+ try {
1578
+ pathname = decodeURIComponent(new URL(request2.url).pathname);
1579
+ } catch {
1580
+ return void 0;
1581
+ }
1582
+ if (pathname === "/" || pathname.includes("\0")) return void 0;
1583
+ const root = resolve(publicPath);
1584
+ const file = resolve(root, `.${pathname}`);
1585
+ if (!file.startsWith(root + sep)) return void 0;
1586
+ if (relative(root, file).split(/[\\/]/).some((segment) => HIDDEN_SEGMENT.test(segment))) return void 0;
1587
+ const info = await stat2(file).catch(() => void 0);
1588
+ if (info === void 0 || !info.isFile()) return void 0;
1589
+ const type = mimeFromExtension(file);
1590
+ const headers = new Headers({
1591
+ "content-type": type.startsWith("text/") || type === "application/json" ? `${type}; charset=utf-8` : type,
1592
+ "content-length": String(info.size),
1593
+ "last-modified": info.mtime.toUTCString()
1594
+ });
1595
+ if (pathname.startsWith("/build/assets/")) headers.set("cache-control", "public, max-age=31536000, immutable");
1596
+ if (request2.method === "HEAD") return new Response(null, { status: 200, headers });
1597
+ const body = Readable2.toWeb(createReadStream2(file));
1598
+ return new Response(body, { status: 200, headers });
1599
+ }
1600
+
1601
+ // server/types.ts
1420
1602
  async function terminateRequest(app, request2, response2) {
1421
1603
  try {
1422
1604
  if (app.bound(HttpKernel)) await app.make(HttpKernel).terminate?.(request2, response2);
@@ -1424,17 +1606,21 @@ async function terminateRequest(app, request2, response2) {
1424
1606
  console.error(error);
1425
1607
  }
1426
1608
  }
1427
- async function handleSafely(app, request2) {
1609
+ async function handleSafely(app, request2, publicPath) {
1428
1610
  try {
1611
+ if (publicPath !== void 0) {
1612
+ const file = await servePublicFile(publicPath, request2);
1613
+ if (file !== void 0) return file;
1614
+ }
1429
1615
  return await app.handle(request2);
1430
1616
  } catch (error) {
1431
1617
  console.error(error);
1432
1618
  return plainServerError();
1433
1619
  }
1434
1620
  }
1435
- function exitOnSignals(close, closed) {
1621
+ function exitOnSignals(close, closed, app) {
1436
1622
  const onSignal = () => {
1437
- void close().then(() => process.exit(0));
1623
+ void close().then(() => app.shutdown()).then(() => process.exit(0));
1438
1624
  };
1439
1625
  process.once("SIGINT", onSignal);
1440
1626
  process.once("SIGTERM", onSignal);
@@ -1453,13 +1639,13 @@ async function serveBun(app, options) {
1453
1639
  const bun = bunRuntime();
1454
1640
  if (bun === void 0) throw new HttpServerError("Bun.serve is not available: the process is not running on Bun.");
1455
1641
  let markClosed = () => void 0;
1456
- const closed = new Promise((resolve) => markClosed = resolve);
1642
+ const closed = new Promise((resolve2) => markClosed = resolve2);
1457
1643
  const server = bun.serve({
1458
1644
  port: options.port,
1459
1645
  hostname: options.hostname,
1460
1646
  async fetch(request2, current) {
1461
1647
  setClientAddress(request2, current.requestIP(request2)?.address);
1462
- const response2 = await handleSafely(app, request2);
1648
+ const response2 = await handleSafely(app, request2, options.publicPath);
1463
1649
  setTimeout(() => void terminateRequest(app, request2, response2), 0);
1464
1650
  return response2;
1465
1651
  },
@@ -1471,14 +1657,14 @@ async function serveBun(app, options) {
1471
1657
  let closing;
1472
1658
  const close = () => closing ??= (async () => {
1473
1659
  let timer;
1474
- const forced = new Promise((resolve) => {
1475
- timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve), options.shutdownTimeout);
1660
+ const forced = new Promise((resolve2) => {
1661
+ timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve2), options.shutdownTimeout);
1476
1662
  });
1477
1663
  await Promise.race([Promise.resolve(server.stop(false)), forced]);
1478
1664
  clearTimeout(timer);
1479
1665
  markClosed();
1480
1666
  })();
1481
- if (options.signals) exitOnSignals(close, closed);
1667
+ if (options.signals) exitOnSignals(close, closed, app);
1482
1668
  return {
1483
1669
  runtime: "bun",
1484
1670
  hostname: options.hostname,
@@ -1491,7 +1677,7 @@ async function serveBun(app, options) {
1491
1677
 
1492
1678
  // server/node.ts
1493
1679
  import { createServer } from "http";
1494
- import { Readable as Readable2 } from "stream";
1680
+ import { Readable as Readable3 } from "stream";
1495
1681
  import { pipeline } from "stream/promises";
1496
1682
  function requestBody(req) {
1497
1683
  let finished = false;
@@ -1573,7 +1759,7 @@ async function writeResponse(res, response2) {
1573
1759
  res.end();
1574
1760
  return;
1575
1761
  }
1576
- await pipeline(Readable2.fromWeb(response2.body), res);
1762
+ await pipeline(Readable3.fromWeb(response2.body), res);
1577
1763
  }
1578
1764
  async function serveNode(app, options) {
1579
1765
  const fallbackHost = `${displayHost(options.hostname)}:${options.port}`;
@@ -1588,7 +1774,7 @@ async function serveNode(app, options) {
1588
1774
  return;
1589
1775
  }
1590
1776
  setClientAddress(request2, req.socket.remoteAddress);
1591
- const response2 = await handleSafely(app, request2);
1777
+ const response2 = await handleSafely(app, request2, options.publicPath);
1592
1778
  try {
1593
1779
  await writeResponse(res, response2);
1594
1780
  } catch {
@@ -1597,18 +1783,18 @@ async function serveNode(app, options) {
1597
1783
  await terminateRequest(app, request2, response2);
1598
1784
  })();
1599
1785
  });
1600
- await new Promise((resolve, reject) => {
1786
+ await new Promise((resolve2, reject) => {
1601
1787
  server.once("error", reject);
1602
1788
  server.listen(options.port, options.hostname, () => {
1603
1789
  server.off("error", reject);
1604
- resolve();
1790
+ resolve2();
1605
1791
  });
1606
1792
  });
1607
1793
  const address = server.address();
1608
- const closed = new Promise((resolve) => server.once("close", () => resolve()));
1794
+ const closed = new Promise((resolve2) => server.once("close", () => resolve2()));
1609
1795
  let closing;
1610
1796
  const close = () => closing ??= (async () => {
1611
- const done = new Promise((resolve) => server.close(() => resolve()));
1797
+ const done = new Promise((resolve2) => server.close(() => resolve2()));
1612
1798
  server.closeIdleConnections();
1613
1799
  const idle = setInterval(() => server.closeIdleConnections(), 50);
1614
1800
  const force = setTimeout(() => server.closeAllConnections(), options.shutdownTimeout);
@@ -1616,7 +1802,7 @@ async function serveNode(app, options) {
1616
1802
  clearInterval(idle);
1617
1803
  clearTimeout(force);
1618
1804
  })();
1619
- if (options.signals) exitOnSignals(close, closed);
1805
+ if (options.signals) exitOnSignals(close, closed, app);
1620
1806
  return {
1621
1807
  runtime: "node",
1622
1808
  hostname: options.hostname,
@@ -1636,7 +1822,8 @@ async function serve(app, options = {}) {
1636
1822
  port: options.port ?? Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3),
1637
1823
  hostname: options.hostname ?? "127.0.0.1",
1638
1824
  signals: options.signals ?? true,
1639
- shutdownTimeout: options.shutdownTimeout ?? 1e4
1825
+ shutdownTimeout: options.shutdownTimeout ?? 1e4,
1826
+ ...options.publicPath !== false && { publicPath: options.publicPath ?? app.publicPath() }
1640
1827
  };
1641
1828
  await app.boot();
1642
1829
  const runtime = options.runtime ?? detectRuntime();
@@ -1647,12 +1834,255 @@ async function serve(app, options = {}) {
1647
1834
  }
1648
1835
 
1649
1836
  // commands.ts
1650
- import { existsSync } from "fs";
1837
+ import { existsSync as existsSync3 } from "fs";
1651
1838
  import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
1652
1839
  import { dirname } from "path";
1653
1840
  import {
1654
1841
  Router as Router3
1655
1842
  } from "@orieljs/contracts";
1843
+
1844
+ // watch/environment.ts
1845
+ import { existsSync, readFileSync } from "fs";
1846
+ import { join } from "path";
1847
+ import * as util from "util";
1848
+ var launchEnvironment = Object.freeze({ ...process.env });
1849
+ var BUN_ENV_FILES = [
1850
+ ".env",
1851
+ ".env.development",
1852
+ ".env.production",
1853
+ ".env.test",
1854
+ ".env.local",
1855
+ ".env.development.local",
1856
+ ".env.production.local",
1857
+ ".env.test.local"
1858
+ ];
1859
+ var expand = (value, env) => value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => env[braced ?? bare] ?? "");
1860
+ function serverEnvironment(options = {}) {
1861
+ const launched = options.launched ?? launchEnvironment;
1862
+ const env = { ...launched };
1863
+ if (!(options.bun ?? process.versions.bun !== void 0)) return env;
1864
+ const parseEnv2 = "parseEnv" in options ? options.parseEnv : util.parseEnv;
1865
+ if (typeof parseEnv2 !== "function") return env;
1866
+ const cwd = options.cwd ?? process.cwd();
1867
+ for (const name of BUN_ENV_FILES) {
1868
+ const file = join(cwd, name);
1869
+ if (!existsSync(file)) continue;
1870
+ let values;
1871
+ try {
1872
+ values = parseEnv2(readFileSync(file, "utf8"));
1873
+ } catch {
1874
+ continue;
1875
+ }
1876
+ for (const [key, raw] of Object.entries(values)) {
1877
+ const value = launched[key];
1878
+ if (raw !== void 0 && value !== void 0 && (value === raw || value === expand(raw, launched))) delete env[key];
1879
+ }
1880
+ }
1881
+ return env;
1882
+ }
1883
+
1884
+ // watch/files.ts
1885
+ import { existsSync as existsSync2, statSync, watch as watchPath } from "fs";
1886
+ import { join as join2 } from "path";
1887
+ var WATCHED_FOLDERS = ["app", "bootstrap", "config", "lang", "resources/lang", "resources/views", "routes"];
1888
+ var WATCHED_ROOT_FILES = ["package.json", "tsconfig.json"];
1889
+ var IGNORED_FOLDERS = ["bootstrap/ssr", "bootstrap/cache"];
1890
+ var CODE_FILE = /\.(?:[cm]?[jt]sx?|json)$/;
1891
+ var DECLARATION_FILE = /\.d\.[cm]?ts$/;
1892
+ var EDITOR_FILE = /(?:\.sw[a-px]|~|___jb_(?:tmp|old)___)$/;
1893
+ var isEnvironmentFile = (name) => (name === ".env" || name.startsWith(".env.")) && name !== ".env.example" && !EDITOR_FILE.test(name);
1894
+ function isWatchedPath(path) {
1895
+ const segments = path.split("/");
1896
+ if (segments.length === 1) return isEnvironmentFile(path) || WATCHED_ROOT_FILES.includes(path);
1897
+ if (!WATCHED_FOLDERS.some((folder) => path.startsWith(`${folder}/`))) return false;
1898
+ if (IGNORED_FOLDERS.some((folder) => path === folder || path.startsWith(`${folder}/`))) return false;
1899
+ if (segments.some((segment) => segment.startsWith(".") || segment === "node_modules")) return false;
1900
+ const name = segments.at(-1);
1901
+ if (DECLARATION_FILE.test(name)) return false;
1902
+ return CODE_FILE.test(name) || !name.includes(".");
1903
+ }
1904
+ var toSlashes = (file) => file.split("\\").join("/");
1905
+ function watchFiles(basePath, onChange, options = {}) {
1906
+ const watch = options.watch ?? ((path, watchOptions, listener) => watchPath(path, watchOptions, listener));
1907
+ const debounce = options.debounce ?? 200;
1908
+ const onError = options.onError ?? (() => {
1909
+ });
1910
+ const changed = /* @__PURE__ */ new Set();
1911
+ let timer;
1912
+ const settle = options.settle ?? 1e3;
1913
+ const startedAt = Date.now();
1914
+ const reportedLate = (path) => {
1915
+ if (Date.now() - startedAt >= settle) return false;
1916
+ try {
1917
+ const modified = statSync(join2(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
1918
+ return modified === void 0 || modified < startedAt + 1;
1919
+ } catch {
1920
+ return false;
1921
+ }
1922
+ };
1923
+ const record = (path) => {
1924
+ if (!isWatchedPath(path) || reportedLate(path)) return;
1925
+ changed.add(path);
1926
+ clearTimeout(timer);
1927
+ timer = setTimeout(() => {
1928
+ const files = [...changed];
1929
+ changed.clear();
1930
+ onChange(files);
1931
+ }, debounce);
1932
+ };
1933
+ const handles = [];
1934
+ const start = (folder, recursive, toPath) => {
1935
+ try {
1936
+ const handle = watch(folder === "." ? basePath : join2(basePath, folder), { recursive }, (_event, file) => {
1937
+ const path = toPath(file);
1938
+ if (path !== void 0) record(path);
1939
+ });
1940
+ handle.on("error", (error) => onError(folder, error));
1941
+ handles.push(handle);
1942
+ } catch (error) {
1943
+ onError(folder, error);
1944
+ }
1945
+ };
1946
+ start(".", false, (file) => file === null ? void 0 : toSlashes(file));
1947
+ for (const folder of WATCHED_FOLDERS) {
1948
+ if (existsSync2(join2(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
1949
+ }
1950
+ return {
1951
+ close() {
1952
+ clearTimeout(timer);
1953
+ changed.clear();
1954
+ for (const handle of handles) handle.close();
1955
+ }
1956
+ };
1957
+ }
1958
+
1959
+ // watch/port.ts
1960
+ import { createServer as createServer2 } from "net";
1961
+ function claimPort(hostname, port) {
1962
+ return new Promise((resolve2, reject) => {
1963
+ const server = createServer2();
1964
+ server.once("error", reject);
1965
+ try {
1966
+ server.listen({ port, host: hostname, exclusive: true }, () => {
1967
+ const { port: claimed } = server.address();
1968
+ server.close(() => resolve2(claimed));
1969
+ });
1970
+ } catch (error) {
1971
+ reject(error);
1972
+ }
1973
+ });
1974
+ }
1975
+ var REASONS = {
1976
+ EADDRINUSE: "Address already in use",
1977
+ EACCES: "Permission denied",
1978
+ EADDRNOTAVAIL: "Can't assign requested address"
1979
+ };
1980
+ function listenFailure(hostname, port, error) {
1981
+ const { code, message } = error;
1982
+ return `Failed to listen on ${displayHost(hostname)}:${port} (reason: ${code !== void 0 && REASONS[code] || message})`;
1983
+ }
1984
+
1985
+ // watch/WatchedServer.ts
1986
+ import { spawn as spawnProcess } from "child_process";
1987
+ var WatchedServer = class {
1988
+ constructor(options) {
1989
+ this.options = options;
1990
+ }
1991
+ options;
1992
+ running;
1993
+ restarting = false;
1994
+ stopping = false;
1995
+ finish = () => {
1996
+ };
1997
+ /** Starts the server and keeps it current until stopped. Resolves 0 once the last server has exited. */
1998
+ run() {
1999
+ const done = new Promise((resolve2) => this.finish = resolve2);
2000
+ const stopListening = this.listenForSignals();
2001
+ const watcher = this.options.watch((files) => void this.changed(files));
2002
+ this.start();
2003
+ return done.finally(() => {
2004
+ watcher.close();
2005
+ stopListening();
2006
+ });
2007
+ }
2008
+ /** Stops the server and ends run(); called again, kills the server at once. */
2009
+ stop() {
2010
+ if (this.stopping) {
2011
+ this.running?.child.kill("SIGKILL");
2012
+ return;
2013
+ }
2014
+ this.stopping = true;
2015
+ void this.stopServer().then(() => this.finish(0));
2016
+ }
2017
+ async changed(files) {
2018
+ if (this.stopping || this.restarting || files.length === 0) return;
2019
+ this.restarting = true;
2020
+ try {
2021
+ this.options.info(
2022
+ files.some(isEnvironmentFile) ? "Environment modified. Restarting server..." : `Application change detected in [${files[0]}]. Restarting server...`
2023
+ );
2024
+ await this.stopServer();
2025
+ if (!this.stopping) this.start();
2026
+ } finally {
2027
+ this.restarting = false;
2028
+ }
2029
+ }
2030
+ start() {
2031
+ const [command, ...args] = this.options.command;
2032
+ const child = (this.options.spawn ?? spawnProcess)(command, args, {
2033
+ cwd: this.options.cwd,
2034
+ env: this.options.env,
2035
+ stdio: ["ignore", "inherit", "inherit"]
2036
+ });
2037
+ const running = { child, stopping: false, killed: false, exited: Promise.resolve() };
2038
+ running.exited = new Promise((resolve2) => {
2039
+ let settled = false;
2040
+ const settle = (report) => {
2041
+ if (settled) return;
2042
+ settled = true;
2043
+ if (this.running === running) this.running = void 0;
2044
+ if (!running.stopping && !this.stopping) this.options.error(`${report}. Waiting for changes before restarting...`);
2045
+ resolve2();
2046
+ };
2047
+ child.once("exit", (code, signal) => settle(signal !== null ? `The server stopped (signal ${signal})` : `The server stopped (exit code ${code})`));
2048
+ child.once("error", (error) => {
2049
+ if (child.pid === void 0) settle(`The server could not start (${error.message})`);
2050
+ });
2051
+ });
2052
+ this.running = running;
2053
+ }
2054
+ /** Sends SIGTERM once and waits for the exit, killing the server after stopTimeout. */
2055
+ async stopServer() {
2056
+ const running = this.running;
2057
+ if (running === void 0) return;
2058
+ if (!running.stopping) {
2059
+ running.stopping = true;
2060
+ running.child.kill("SIGTERM");
2061
+ }
2062
+ const timeout = this.options.stopTimeout ?? 15e3;
2063
+ const timer = setTimeout(() => {
2064
+ if (running.killed) return;
2065
+ running.killed = true;
2066
+ this.options.error(`The server did not stop within ${timeout / 1e3} seconds and was killed.`);
2067
+ running.child.kill("SIGKILL");
2068
+ }, timeout);
2069
+ try {
2070
+ await running.exited;
2071
+ } finally {
2072
+ clearTimeout(timer);
2073
+ }
2074
+ }
2075
+ listenForSignals() {
2076
+ const signals = this.options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
2077
+ const handler = () => this.stop();
2078
+ for (const signal of signals) process.on(signal, handler);
2079
+ return () => {
2080
+ for (const signal of signals) process.off(signal, handler);
2081
+ };
2082
+ }
2083
+ };
2084
+
2085
+ // commands.ts
1656
2086
  function io(command) {
1657
2087
  return {
1658
2088
  option: (name) => {
@@ -1726,11 +2156,36 @@ ${web[1]}api: 'routes/api.ts',`);
1726
2156
  ${create[0]}`);
1727
2157
  return void 0;
1728
2158
  }
2159
+ async function serveWatching(app, out, port, run) {
2160
+ const hostname = out.option("host") ?? "127.0.0.1";
2161
+ const requested = port !== void 0 ? Number(port) : Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3);
2162
+ let claimed;
2163
+ try {
2164
+ claimed = await claimPort(hostname, requested);
2165
+ } catch (error) {
2166
+ out.error(listenFailure(hostname, requested, error));
2167
+ return 1;
2168
+ }
2169
+ const script = process.argv[1];
2170
+ if (script === void 0) {
2171
+ out.error("serve --watch can't tell which script started this process, so it can't start the server.");
2172
+ return 1;
2173
+ }
2174
+ return run({
2175
+ command: [process.execPath, ...process.execArgv, script, "serve", `--host=${hostname}`, `--port=${claimed}`],
2176
+ cwd: process.cwd(),
2177
+ env: serverEnvironment(),
2178
+ watch: (onChange) => watchFiles(app.basePath(), onChange, { onError: (folder, error) => out.warn(`Unable to watch [${folder}] for changes: ${error.message}`) }),
2179
+ info: (message) => out.info(message),
2180
+ error: (message) => out.error(message)
2181
+ });
2182
+ }
1729
2183
  function registerHttpCommands(kernel, app, options = {}) {
1730
2184
  const serveApp = options.serve ?? serve;
1731
2185
  const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
2186
+ const runWatching = options.watch ?? ((watchOptions) => new WatchedServer(watchOptions).run());
1732
2187
  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}",
2188
+ "serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on} {--watch : Automatically restart the server when the application is modified}",
1734
2189
  async function() {
1735
2190
  const out = io(this);
1736
2191
  const port = out.option("port");
@@ -1738,6 +2193,7 @@ function registerHttpCommands(kernel, app, options = {}) {
1738
2193
  out.error("The --port option must be a number.");
1739
2194
  return 1;
1740
2195
  }
2196
+ if (out.flag("watch")) return serveWatching(app, out, port, runWatching);
1741
2197
  const server = await serveApp(app, {
1742
2198
  hostname: out.option("host") ?? "127.0.0.1",
1743
2199
  ...port !== void 0 && { port: Number(port) },
@@ -1785,7 +2241,7 @@ function registerHttpCommands(kernel, app, options = {}) {
1785
2241
  kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
1786
2242
  const out = io(this);
1787
2243
  const routesFile = app.basePath("routes/api.ts");
1788
- if (existsSync(routesFile) && !out.flag("force")) {
2244
+ if (existsSync3(routesFile) && !out.flag("force")) {
1789
2245
  out.error("API routes file already exists.");
1790
2246
  } else {
1791
2247
  await mkdir(dirname(routesFile), { recursive: true });
@@ -1793,7 +2249,7 @@ function registerHttpCommands(kernel, app, options = {}) {
1793
2249
  out.info("Published API routes file.");
1794
2250
  }
1795
2251
  const bootstrapFile = app.bootstrapPath("app.ts");
1796
- const source = existsSync(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
2252
+ const source = existsSync3(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
1797
2253
  const updated = source === void 0 ? void 0 : addApiRouting(source);
1798
2254
  if (updated === void 0) {
1799
2255
  out.warn(
@@ -1802,7 +2258,7 @@ function registerHttpCommands(kernel, app, options = {}) {
1802
2258
  } else if (updated !== source) {
1803
2259
  await writeFile(bootstrapFile, updated);
1804
2260
  }
1805
- out.info("API scaffolding installed. Run [oriel migrate] to create the personal access tokens table.");
2261
+ out.info("API scaffolding installed.");
1806
2262
  return 0;
1807
2263
  }).purpose("Create an API routes file and prepare the application for API authentication");
1808
2264
  }
@@ -1816,6 +2272,7 @@ var HttpServiceProvider = class extends ServiceProvider {
1816
2272
  {
1817
2273
  trusted_proxies: [],
1818
2274
  max_body_size: 8 * 1024 * 1024,
2275
+ max_json_depth: 512,
1819
2276
  csrf_except: [],
1820
2277
  redirect_guests_to: void 0,
1821
2278
  redirect_users_to: void 0
@@ -1870,6 +2327,7 @@ export {
1870
2327
  debugPage,
1871
2328
  detectRuntime,
1872
2329
  escapeHtml,
2330
+ exceedsJsonDepth,
1873
2331
  extensionFromMime,
1874
2332
  filterRouteList,
1875
2333
  formatRouteList,
@@ -1895,6 +2353,7 @@ export {
1895
2353
  serve,
1896
2354
  serveBun,
1897
2355
  serveNode,
2356
+ servePublicFile,
1898
2357
  setClientAddress,
1899
2358
  stackFrames,
1900
2359
  statusFor,