@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/NOTICE CHANGED
@@ -7,3 +7,10 @@ This product includes software developed by the Oriel project
7
7
  Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
8
  The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
9
  (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
10
+
11
+ @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
+ voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
13
+
14
+ @orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
15
+ Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
16
+ Copyright (c) Taylor Otwell, MIT License.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @orieljs/http
2
+
3
+ The HTTP layer of Oriel: the request, responses and redirects, the middleware kernel, the error handler and its error pages, and the server for Node and Bun. It is Oriel's counterpart of Laravel's HTTP layer. Http is part of `@orieljs/framework`, which every Oriel app already installs, and is re-exported from it. In an app, import it from `@orieljs/framework`; `@orieljs/http` is also published on its own.
4
+
5
+ ```bash
6
+ npm install @orieljs/framework
7
+ ```
8
+
9
+ ```ts title="app/Http/Controllers/SearchController.ts"
10
+ import { request, response } from '@orieljs/framework'
11
+ import { Post } from '../../Models/Post'
12
+ import { Controller } from './Controller'
13
+
14
+ export class SearchController extends Controller {
15
+ async index() {
16
+ const term = request().input('q', '')
17
+ const posts = await Post.whereLike('title', `%${term}%`).latest().get()
18
+ return response().json({ term, posts }, 200, { 'Cache-Control': 'no-store' })
19
+ }
20
+ }
21
+ ```
22
+
23
+ `request()` is the current `IncomingRequest`, with `input()`, `string()`, `boolean()`, `integer()`, `only()`, `file()` and `header()`; `await request().user()` is the signed-in user, or `undefined` for a guest. `response()`, `redirect()` and `back()` build responses, and `abort()`, `abortIf()` and `abortUnless()` stop a request with an HTTP error. `await serve(app)` runs the app on Node or Bun.
24
+
25
+ Documentation: https://orieljs.vercel.app/docs/requests
26
+
27
+ Status: early alpha. APIs will change.
28
+
29
+ ## License
30
+
31
+ Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
32
+
33
+ Website: https://orieljs.vercel.app
package/TRADEMARKS.md ADDED
@@ -0,0 +1,33 @@
1
+ # Oriel trademark policy
2
+
3
+ The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
4
+ the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
5
+ what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
6
+ and what isn't.
7
+
8
+ ## You can, without asking
9
+
10
+ - Use, change, ship and sell the code, as the license allows.
11
+ - Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
12
+ - Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
13
+ it is not official (for example, don't publish it under the `@orieljs` npm scope).
14
+ - Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
15
+ - Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
16
+ distortion).
17
+
18
+ ## You can't, without written permission
19
+
20
+ - Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
21
+ - Use the name or logo as the name, logo or domain of your own product, company or service.
22
+ - Suggest that the Oriel project made, endorses or supports your work when it doesn't.
23
+ - Sell merchandise with the name or logo.
24
+
25
+ ## Forks
26
+
27
+ Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
28
+ it is based on Oriel.
29
+
30
+ ## Questions
31
+
32
+ Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
33
+ is fine, ask; the answer is usually yes.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { HttpRequest, CookieJar, Session, Container, RedirectResponse, HeadersInit, Redirector, UploadedFile, Rules, OrielError, ResponseFactory, BodyInit, UrlGenerator, HttpKernel, Application, MiddlewareClass, Middleware, Next, MiddlewareConfigurator, Router, ConfigRepository, ExceptionHandler, ExceptionsConfigurator, ConsoleKernel, Route } from '@orieljs/contracts';
1
+ import { HttpRequest, CookieJar, Session, Container, RedirectResponse, HeadersInit, MessageBag, Redirector, UploadedFile, Rules, InferValidatedType, OrielError, RouteName, RouteArguments, ResponseFactory, BodyInit, UrlGenerator, HttpKernel, Application, MiddlewareClass, Middleware, Next, MiddlewareConfigurator, Router, ConfigRepository, ExceptionHandler, ExceptionsConfigurator, ConsoleKernel, Route } from '@orieljs/contracts';
2
+ import { FileType, Stringable, ServiceProvider } from '@orieljs/support';
2
3
  import { IncomingMessage, ServerResponse } from 'node:http';
3
- import { ServiceProvider } from '@orieljs/support';
4
+ import { SpawnOptions, ChildProcess } from 'node:child_process';
4
5
 
6
+ /**
7
+ * Whether a JSON text nests objects and arrays deeper than `maxDepth`, found by scanning (no parsing, no recursion):
8
+ * brackets inside strings don't count. The kernel answers 400 for such bodies (http.max_json_depth) before the
9
+ * recursive input handling (sanitizing, TrimStrings, validation) could overflow the stack.
10
+ */
11
+ declare function exceedsJsonDepth(text: string, maxDepth: number): boolean;
5
12
  /** Reads a request body into memory, refusing more than `limit` bytes (413), like PHP's post_max_size. */
6
13
  declare function readBody(raw: Request, limit: number | null): Promise<Uint8Array>;
7
14
 
@@ -12,9 +19,7 @@ declare function currentRequest(): HttpRequest | undefined;
12
19
  declare function optionalSession(): Session | undefined;
13
20
  declare function optionalCookieJar(): CookieJar | undefined;
14
21
 
15
- type ErrorsInput = Record<string, string | string[]> | {
16
- toObject(): Record<string, string[]>;
17
- };
22
+ type ErrorsInput = Record<string, string | string[]> | MessageBag;
18
23
  /**
19
24
  * Laravel's RedirectResponse. with()/withErrors()/withInput() flash to the request's Session immediately, like
20
25
  * Laravel; without a bound Session (an API-only app) they are skipped silently.
@@ -46,7 +51,10 @@ declare class HttpRedirector implements Redirector {
46
51
  /** Pulls url.intended from the session (put there by guest()), else goes to `fallback`. */
47
52
  intended(fallback?: string, status?: number): HttpRedirectResponse;
48
53
  away(url: string, status?: number): HttpRedirectResponse;
49
- /** Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. */
54
+ /**
55
+ * Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. Only the path
56
+ * is stored, so intended() goes back on whichever host serves the app, and never to another site's Referer.
57
+ */
50
58
  guest(path: string, status?: number): HttpRedirectResponse;
51
59
  setIntendedUrl(url: string): void;
52
60
  refresh(status?: number): HttpRedirectResponse;
@@ -58,13 +66,26 @@ declare function clientAddress(request: Request): string | undefined;
58
66
  /** An uploaded file from a multipart body. store()/storeAs() go through the FilesystemManager. */
59
67
  declare class FormUploadedFile implements UploadedFile {
60
68
  private readonly file;
69
+ private readonly detected?;
61
70
  readonly clientName: string;
62
71
  readonly mimeType: string;
63
72
  readonly size: number;
73
+ /**
74
+ * Reads the leading bytes first, so extension(), hashName() and store() name the file by its content. The request
75
+ * builds its uploads this way.
76
+ */
77
+ static fromFile(file: Blob & {
78
+ readonly name?: string;
79
+ }): Promise<FormUploadedFile>;
64
80
  constructor(file: Blob & {
65
81
  readonly name?: string;
66
- });
67
- /** Guessed from the MIME type, falling back to the client's extension (Laravel's extension()). */
82
+ }, detected?: FileType.DetectedType | undefined);
83
+ /**
84
+ * Laravel's extension(): the type the bytes show (guessExtension()), when they were read and match a known format.
85
+ * Text and bytes no format matches keep the client's claim (its MIME type, else its file name) when it is a plain
86
+ * extension, unless it names a format the bytes would have shown or one a browser renders or runs: otherwise 'txt'
87
+ * for text, '' for the rest. Unread (built directly), the client's claim when it is a plain extension, else ''.
88
+ */
68
89
  extension(): string;
69
90
  clientExtension(): string;
70
91
  /** A random 40-character name with the guessed extension. */
@@ -81,6 +102,8 @@ interface IncomingRequestOptions {
81
102
  trustedProxies?: readonly string[];
82
103
  /** Largest request body in bytes before a 413 (config http.max_body_size); null is unlimited. Default 8 MB. */
83
104
  maxBodySize?: number | null;
105
+ /** Deepest nesting of objects and arrays in a JSON body before a 400 (config http.max_json_depth); null is unlimited. Default 512. */
106
+ maxJsonDepth?: number | null;
84
107
  }
85
108
  /** Exact match, IPv4 CIDR ('10.0.0.0/8') or '*'. */
86
109
  declare function ipMatches(ip: string, patterns: readonly string[]): boolean;
@@ -136,7 +159,10 @@ declare class IncomingRequest implements HttpRequest {
136
159
  hasAny(...keys: Array<string | string[]>): boolean;
137
160
  missing(...keys: Array<string | string[]>): boolean;
138
161
  filled(key: string): boolean;
139
- string(key: string, defaultValue?: string): string;
162
+ /** Laravel's string(): the input as a Stringable (the default when it is missing or null): request().string('q').trim(). */
163
+ string(key: string, defaultValue?: string): Stringable;
164
+ /** Laravel's str(): string() under its short name. */
165
+ str(key: string, defaultValue?: string): Stringable;
140
166
  /** PHP's FILTER_VALIDATE_BOOLEAN: '1', 'true', 'on', 'yes' (any case) and true are true. */
141
167
  boolean(key: string, defaultValue?: boolean): boolean;
142
168
  /** PHP's intval: leading digits, else 0. */
@@ -155,10 +181,17 @@ declare class IncomingRequest implements HttpRequest {
155
181
  /** One route parameter (after model binding), or all of them. */
156
182
  route<T = unknown>(key?: string): T;
157
183
  session(): Session;
158
- user<T = unknown>(guard?: string): T | undefined;
184
+ /**
185
+ * The signed-in user, through the resolver Auth sets, else the current scope's guard (the default, or `guard`).
186
+ * The guard keeps the user it found, so it is looked up once per request. Undefined for a guest or without auth.
187
+ */
188
+ user<T = unknown>(guard?: string): Promise<T | undefined>;
159
189
  setUserResolver(resolver: (guard?: string) => unknown): void;
160
- /** Validates all() through the ValidationFactory; throws its Validation error on failure. */
161
- validate<T = Record<string, unknown>>(rules: Rules, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<T>;
190
+ /**
191
+ * Validates all() through the ValidationFactory; throws its Validation error on failure. The data is typed from the
192
+ * rules: `validate({ title: 'required|string' })` is `{ title: string }`.
193
+ */
194
+ validate<const R extends Rules>(rules: R, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<InferValidatedType<R>>;
162
195
  }
163
196
 
164
197
  /** Helpers for request input: PHP-style bracket keys, safe cloning (no prototype pollution) and blank checks. */
@@ -209,22 +242,32 @@ declare class HttpServerError extends Error implements OrielError {
209
242
  readonly name = "HttpServerError";
210
243
  }
211
244
 
212
- /** request() → the current HttpRequest; request('key', default) → its input. */
213
- declare function request(): HttpRequest;
245
+ /**
246
+ * request() → the current request (Laravel's Illuminate\Http\Request: the HttpRequest contract plus its extras, such
247
+ * as string()); request('key', default) → its input.
248
+ */
249
+ declare function request(): IncomingRequest;
214
250
  declare function request<T = unknown>(key: string, defaultValue?: T): T;
215
251
  /** response() → the ResponseFactory; response(content, status, headers) → a Response. */
216
252
  declare function response(): ResponseFactory;
217
253
  declare function response(content: BodyInit | null, status?: number, headers?: HeadersInit): Response;
254
+ /** The Redirector with its route() typed by the generated route names and their parameters. */
255
+ type TypedRedirector = Omit<Redirector, 'route'> & {
256
+ route<N extends RouteName>(name: N, ...args: RouteArguments<N, [status?: number]>): RedirectResponse;
257
+ };
218
258
  /** redirect() → the Redirector; redirect('/path', status) → a RedirectResponse. */
219
- declare function redirect(): Redirector;
259
+ declare function redirect(): TypedRedirector;
220
260
  declare function redirect(to: string, status?: number): RedirectResponse;
221
261
  /** Redirect to the previous URL (same argument order as Redirector.back()). */
222
262
  declare function back(fallback?: string, status?: number): RedirectResponse;
223
263
  /** url() → the UrlGenerator; url('/path', query, absolute) → a URL. */
224
264
  declare function url(): UrlGenerator;
225
265
  declare function url(path: string, query?: Record<string, unknown>, absolute?: boolean): string;
226
- /** The URL of a named route. */
227
- declare function route(name: string, parameters?: unknown, absolute?: boolean): string;
266
+ /**
267
+ * The URL of a named route: route('posts.show', post), route('posts.show', { post, page: 2 }). Once `oriel
268
+ * route:types` has run, the name and the route's parameters are checked.
269
+ */
270
+ declare function route<N extends RouteName>(name: N, ...args: RouteArguments<N>): string;
228
271
  /** abort(404), abort(403, 'Nope', headers), or abort(response) to send a ready response. */
229
272
  declare function abort(status: number | Response, message?: string, headers?: Record<string, string>): never;
230
273
  declare function abortIf(condition: unknown, status: number | Response, message?: string, headers?: Record<string, string>): void;
@@ -312,6 +355,7 @@ declare function applyMiddlewareCallbacks(app: Application, kernel: Kernel): voi
312
355
  type ReportCallback = (error: unknown) => void | boolean | Promise<void | boolean>;
313
356
  type RenderCallback = (error: unknown, request: unknown) => Response | undefined | Promise<Response | undefined>;
314
357
  type ErrorClass = abstract new (...args: never[]) => Error;
358
+ type JsonWhenCallback = (request: HttpRequest, error: unknown) => boolean;
315
359
  /** HTTP status for an error (spec §7). */
316
360
  declare function statusFor(error: unknown): number;
317
361
  /** "The title field is required. (and 2 more errors)" (Laravel's ValidationException::summarize). */
@@ -326,6 +370,10 @@ declare class ErrorHandler implements ExceptionHandler {
326
370
  reportUsing(callback: ReportCallback): void;
327
371
  renderUsing(callback: RenderCallback): void;
328
372
  ignore(...classes: ErrorClass[]): void;
373
+ /** Laravel's shouldRenderJsonWhen(): the callback decides JSON or HTML instead of the Accept header. */
374
+ shouldRenderJsonWhen(callback: JsonWhenCallback): void;
375
+ /** Laravel's shouldReturnJson(): the shouldRenderJsonWhen() callback when set, else request.expectsJson(). */
376
+ protected shouldReturnJson(request: HttpRequest, error: unknown): boolean;
329
377
  shouldReport(error: unknown): boolean;
330
378
  /** Report callbacks first (returning false stops the default log), then LogManager.error(), else console.error. Once per error. */
331
379
  report(error: unknown): Promise<void>;
@@ -339,11 +387,16 @@ declare class ExceptionsConfiguration implements ExceptionsConfigurator {
339
387
  report(callback: ReportCallback): this;
340
388
  render(callback: RenderCallback): this;
341
389
  dontReport(...classes: ErrorClass[]): this;
390
+ /** shouldRenderJsonWhen((request) => request.is('api/*')): JSON errors for those requests, whatever they accept. */
391
+ shouldRenderJsonWhen(callback: (request: HttpRequest, error: unknown) => boolean): this;
342
392
  }
343
393
  /** Runs the withExceptions() callbacks against a handler (the provider does this when the handler is built). */
344
394
  declare function applyExceptionCallbacks(app: Application, handler: ErrorHandler): void;
345
395
 
346
- /** Laravel's wording for the status (its error pages say "Server Error" and "Page Expired"). */
396
+ /**
397
+ * The text an error page shows for a status: Laravel's own wording where it has a view ("Server Error", "Page
398
+ * Expired"), else Symfony's `Response::$statusTexts`, which Laravel shows for the others.
399
+ */
347
400
  declare function statusText(status: number): string;
348
401
  declare function escapeHtml(value: string): string;
349
402
  /** Laravel's minimal error page: "404 | Not Found". */
@@ -373,12 +426,16 @@ interface ServeOptions {
373
426
  runtime?: Runtime;
374
427
  /** Called once listening. Default: prints "Server running on [url]." */
375
428
  onListen?: (server: RunningServer) => void;
429
+ /** Serve existing files from this directory before the app (Laravel's server.php). Default: app.publicPath(); false turns it off. */
430
+ publicPath?: string | false;
376
431
  }
377
432
  interface ResolvedServeOptions {
378
433
  readonly port: number;
379
434
  readonly hostname: string;
380
435
  readonly signals: boolean;
381
436
  readonly shutdownTimeout: number;
437
+ /** Where public files are served from, or undefined when turned off. */
438
+ readonly publicPath?: string;
382
439
  }
383
440
  interface RunningServer {
384
441
  readonly runtime: Runtime;
@@ -421,6 +478,35 @@ declare function detectRuntime(): Runtime;
421
478
  /** Boots the app and serves it with node:http or Bun.serve (public/index.ts: `await serve(app)`). */
422
479
  declare function serve(app: Application, options?: ServeOptions): Promise<RunningServer>;
423
480
 
481
+ /**
482
+ * Laravel's server.php for `artisan serve`: a GET or HEAD for a file that exists under public/ is answered
483
+ * with that file, so the built Vite assets (public/build) and other public files work without a web server.
484
+ * Anything else (directories, missing files, other methods) returns undefined and goes to the application.
485
+ */
486
+ declare function servePublicFile(publicPath: string, request: Request): Promise<Response | undefined>;
487
+
488
+ interface FileWatcher {
489
+ close(): void;
490
+ }
491
+
492
+ /** node:child_process's spawn(), replaceable in tests. */
493
+ type SpawnServer = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
494
+ interface WatchedServerOptions {
495
+ /** The server's command line: the runtime, its flags, the CLI script, then `serve --host=… --port=…`. */
496
+ command: readonly string[];
497
+ cwd: string;
498
+ env: NodeJS.ProcessEnv;
499
+ /** Starts watching: calls back with the changed files (relative to the base path) once they settle. */
500
+ watch(onChange: (files: string[]) => void): FileWatcher;
501
+ info(message: string): void;
502
+ error(message: string): void;
503
+ /** How long a server may take to stop before it is killed, in ms. Default 15000 (serve gives requests 10 s). */
504
+ stopTimeout?: number;
505
+ /** The signals that stop it. Default SIGINT, SIGTERM and SIGHUP. */
506
+ signals?: readonly NodeJS.Signals[];
507
+ spawn?: SpawnServer;
508
+ }
509
+
424
510
  interface RouteListRow {
425
511
  /** 'GET|HEAD' */
426
512
  readonly method: string;
@@ -460,6 +546,8 @@ interface HttpCommandOptions {
460
546
  serve?: (app: Application, options: ServeOptions) => Promise<RunningServer>;
461
547
  /** The terminal width for route:list. Default: process.stdout.columns, else 120. */
462
548
  terminalWidth?: () => number;
549
+ /** Replaces `serve --watch`'s restarting server (tests). Resolves the command's exit code. */
550
+ watch?: (options: WatchedServerOptions) => Promise<number>;
463
551
  }
464
552
  /** Registers `serve`, `route:list` and `install:api` as closure commands (PC1). */
465
553
  declare function registerHttpCommands(kernel: ConsoleKernel, app: Application, options?: HttpCommandOptions): void;
@@ -477,4 +565,4 @@ declare class HttpServiceProvider extends ServiceProvider {
477
565
  boot(): void;
478
566
  }
479
567
 
480
- export { API_ROUTES_STUB, ConvertEmptyStringsToNull, type ErrorClass, ErrorHandler, ExceptionsConfiguration, FormUploadedFile, type HttpCommandOptions, HttpException, HttpRedirectResponse, HttpRedirector, HttpResponseException, HttpResponseFactory, HttpServerError, HttpServiceProvider, IncomingRequest, type IncomingRequestOptions, Kernel, MiddlewareConfiguration, type RenderCallback, type ReportCallback, type ResolvedServeOptions, type RouteListFilters, type RouteListRow, type RunningServer, type Runtime, type ServeOptions, type StackFrame, TrimStrings, abort, abortIf, abortUnless, addApiRouting, applyExceptionCallbacks, applyMiddlewareCallbacks, back, bunRuntime, clientAddress, contentDisposition, currentRequest, debugPage, detectRuntime, escapeHtml, extensionFromMime, filterRouteList, formatRouteList, fromEntries, ipMatches, isBlank, mergeRecursive, mimeFromExtension, minimalPage, normalizeLists, optionalCookieJar, optionalSession, parseBracketKey, readBody, redirect, registerHttpCommands, request, requestScope, response, route, routeListRows, sanitize, serve, serveBun, serveNode, setClientAddress, stackFrames, statusFor, statusText, summarizeValidation, toWebRequest, transformLeaves, url, writeResponse };
568
+ export { API_ROUTES_STUB, ConvertEmptyStringsToNull, type ErrorClass, ErrorHandler, ExceptionsConfiguration, FormUploadedFile, type HttpCommandOptions, HttpException, HttpRedirectResponse, HttpRedirector, HttpResponseException, HttpResponseFactory, HttpServerError, HttpServiceProvider, IncomingRequest, type IncomingRequestOptions, type JsonWhenCallback, Kernel, MiddlewareConfiguration, type RenderCallback, type ReportCallback, type ResolvedServeOptions, type RouteListFilters, type RouteListRow, type RunningServer, type Runtime, type ServeOptions, type StackFrame, TrimStrings, type TypedRedirector, abort, abortIf, abortUnless, addApiRouting, applyExceptionCallbacks, applyMiddlewareCallbacks, back, bunRuntime, clientAddress, contentDisposition, currentRequest, debugPage, detectRuntime, escapeHtml, exceedsJsonDepth, extensionFromMime, filterRouteList, formatRouteList, fromEntries, ipMatches, isBlank, mergeRecursive, mimeFromExtension, minimalPage, normalizeLists, optionalCookieJar, optionalSession, parseBracketKey, readBody, redirect, registerHttpCommands, request, requestScope, response, route, routeListRows, sanitize, serve, serveBun, serveNode, servePublicFile, setClientAddress, stackFrames, statusFor, statusText, summarizeValidation, toWebRequest, transformLeaves, url, writeResponse };