@dunx/http 1.1.0 → 1.2.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.
@@ -0,0 +1,58 @@
1
+ import { type Dependency, type ModuleRef } from '@dunx/core';
2
+ export interface RouteInputs {
3
+ readonly body?: string;
4
+ readonly query?: string;
5
+ readonly params?: string;
6
+ }
7
+ export interface RouteNode {
8
+ readonly method: string;
9
+ readonly path: string;
10
+ readonly controller: string;
11
+ readonly handler: string;
12
+ readonly module: string;
13
+ readonly public: boolean;
14
+ readonly roles: readonly string[] | null;
15
+ /** Class-level `@UseGuards` first, then method-level, which is resolution order. */
16
+ readonly guards: readonly string[];
17
+ /** `@ApiHidden()`, so a caller can tell "not documented" from "not there". */
18
+ readonly hidden: boolean;
19
+ /**
20
+ * Which inputs the route validates, and by which Standard Schema vendor. The
21
+ * schemas themselves are not here: turning one into JSON Schema is zod-specific
22
+ * work that `@dunx/openapi` already does, and `dunx_openapi` is where it lives.
23
+ * What this answers is "does this route parse a body at all", which is the
24
+ * question that does not need a schema compiler.
25
+ */
26
+ readonly validates: RouteInputs;
27
+ /** The success status the decorator declared, or null for the default. */
28
+ readonly status: number | null;
29
+ /** Status codes the route documents a response schema for. */
30
+ readonly responses: readonly number[];
31
+ }
32
+ export declare const routesOf: (root: ModuleRef) => readonly RouteNode[];
33
+ /**
34
+ * Gateways are declared in `@Module({ providers })` like any other injectable and
35
+ * found by their marker, so they are read the same way routes are: the class's
36
+ * prototype, never an instance.
37
+ *
38
+ * `discoverGateway` does all of it - the path, the marked methods, the event each
39
+ * message handler claims - and `Object.create(Gateway.prototype)` satisfies its
40
+ * one argument, exactly as it satisfies `discoverRoutes`. Its sibling
41
+ * `discoverGateways` is the one that is unusable here: it takes a `resolve`
42
+ * callback and constructs every gateway, which is the boot this package exists to
43
+ * avoid. Only the bound `invoke` is dropped, being a function.
44
+ */
45
+ export interface GatewayHandler {
46
+ readonly kind: string;
47
+ /** The envelope event a message handler claims; null is the raw catch-all. */
48
+ readonly event: string | null;
49
+ readonly method: string;
50
+ }
51
+ export interface GatewayNode {
52
+ readonly name: string;
53
+ readonly path: string;
54
+ readonly module: string;
55
+ readonly dependencies: readonly Dependency[];
56
+ readonly handlers: readonly GatewayHandler[];
57
+ }
58
+ export declare const gatewaysOf: (root: ModuleRef) => readonly GatewayNode[];
@@ -11,7 +11,7 @@ export interface RequestLoggingOptions {
11
11
  *
12
12
  * Reading it means `req.clone().text()` - a second copy of every payload,
13
13
  * buffered and parsed, on the hot path. Measured on the `validate` scenario in
14
- * `tools/bench`, turning both body options on costs roughly two thirds of the
14
+ * `internal/bench`, turning both body options on costs roughly two thirds of the
15
15
  * throughput. It is also the field most likely to contain a password.
16
16
  *
17
17
  * Turn it on in development, where seeing the payload is the point.
@@ -27,6 +27,23 @@ export interface RequestLoggingOptions {
27
27
  * is what makes it free. `correlateIgnored` buys the correlation back.
28
28
  */
29
29
  readonly ignore?: readonly string[];
30
+ /**
31
+ * Path **prefixes** to skip, for a whole mount rather than one path.
32
+ *
33
+ * `ignore` is an exact-match `Set` because that is one lookup on the hot path
34
+ * and a health check is one path. A mount is not: `@dunx/dashboard` at
35
+ * `/_dunx` polls four endpoints every five seconds and bull-board pulls a
36
+ * dozen assets, and listing them is both tedious and wrong the moment either
37
+ * grows an endpoint.
38
+ *
39
+ * Scanned only when non-empty, so an app that sets none pays nothing - the
40
+ * same guard `ignore` has. Keep the list short; it is a loop.
41
+ *
42
+ * ```ts
43
+ * requestLogging: { ignorePrefix: ['/_dunx'] }
44
+ * ```
45
+ */
46
+ readonly ignorePrefix?: readonly string[];
30
47
  /**
31
48
  * Keep the request id and the async scope on an `ignore`d path. Default
32
49
  * **`false`**.
@@ -0,0 +1,35 @@
1
+ import type { BunRequest } from 'bun';
2
+ import type { Middleware, Next } from '../server/middleware.js';
3
+ import type { RouteContext } from '../server/context.js';
4
+ import { StaticOptions } from './options.js';
5
+ /**
6
+ * Static files, on `Bun.file`.
7
+ *
8
+ * Nest has `ServeStaticModule` over `serve-static`, which is Express middleware
9
+ * doing its own `stat`, its own range parsing, its own ETag and its own MIME table.
10
+ * None of that is needed here: `Bun.file(path)` handed to a `Response` already
11
+ * streams, already sets `content-type` from the extension, already answers a
12
+ * `Range` request, and does the whole thing with `sendfile(2)` rather than reading
13
+ * into JavaScript. So this file is a **path check and a cache policy**, and that is
14
+ * the entire justification for it existing.
15
+ *
16
+ * A middleware rather than routes, for the same reason the dashboard is one: the
17
+ * file set is whatever is on disk at request time, and turning it into a
18
+ * `Bun.serve` route table would mean walking a directory at boot and being wrong
19
+ * the moment anything changed.
20
+ */
21
+ export declare class StaticFiles implements Middleware {
22
+ #private;
23
+ constructor(options: StaticOptions);
24
+ /**
25
+ * The file for a request path, or `undefined` if it escapes the root.
26
+ *
27
+ * **The traversal check is the point of this method.** `..` segments are removed
28
+ * by `normalize`, but that alone is not enough: a root of `/srv/app` and a
29
+ * request for `/srv/app-secrets` both start with the same string, so the guard
30
+ * has to compare against the root **with a separator**. Both halves have to hold
31
+ * or a caller reads the filesystem.
32
+ */
33
+ resolvePath(pathname: string): string | undefined;
34
+ handle(req: BunRequest, _ctx: RouteContext, next: Next): Promise<Response>;
35
+ }
@@ -0,0 +1,27 @@
1
+ import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
2
+ import { type StaticOptionsInit } from './options.js';
3
+ /**
4
+ * Serves a directory, the way Nest's `ServeStaticModule` does - and like the
5
+ * dashboard, **it does not register itself**. The app does:
6
+ *
7
+ * ```ts
8
+ * const app = await HttpFactory.create(AppModule);
9
+ * app.use(StaticFiles);
10
+ * ```
11
+ *
12
+ * Position in the chain is the decision being left to the app. Static assets
13
+ * usually want to be *outside* an auth guard and *inside* request logging, and no
14
+ * default can know which. Anything outside the mount falls through untouched, so
15
+ * the app's own routes and its 404 behave exactly as before.
16
+ *
17
+ * There is no `index.html` fallback and no SPA rewrite. Both are one route in the
18
+ * app - `@Get('/*')` returning `Bun.file(...)` - and building them in would mean
19
+ * this middleware deciding what a 404 means for paths it does not own.
20
+ */
21
+ export declare class StaticModule {
22
+ static forRoot(init: StaticOptionsInit): DynamicModule;
23
+ /** `forRoot` with the root read off the container - a config value, usually. */
24
+ static forRootAsync<const D extends Deps>(config: FactoryProvider<StaticOptionsInit, D> & {
25
+ readonly imports?: DynamicModule['imports'];
26
+ }): DynamicModule;
27
+ }
@@ -0,0 +1,46 @@
1
+ export interface StaticOptionsInit {
2
+ /**
3
+ * The directory served. Resolved once, at construction, and every request is
4
+ * checked against it - see `StaticFiles.resolvePath`.
5
+ */
6
+ readonly root: string;
7
+ /**
8
+ * The URL prefix it is served under. `/` serves from the root of the app, which
9
+ * is the usual case for a `public/` directory.
10
+ *
11
+ * @default '/'
12
+ */
13
+ readonly path?: string;
14
+ /**
15
+ * `max-age` in seconds for anything `immutable` does not claim.
16
+ *
17
+ * Deliberately short. A long max-age on a name that can change is a promise the
18
+ * server cannot keep, and the fix - a content hash in the filename - is the
19
+ * thing `immutable` is for.
20
+ *
21
+ * @default 60
22
+ */
23
+ readonly maxAge?: number;
24
+ /**
25
+ * Which paths may be cached forever.
26
+ *
27
+ * Only honest for a **content-addressed** name, where a change produces a
28
+ * different URL: `(path) => /\.[0-9a-f]{8}\.(js|css)$/.test(path)`. The default
29
+ * claims nothing, because guessing wrong here is a stale asset nobody can flush.
30
+ */
31
+ readonly immutable?: (pathname: string) => boolean;
32
+ }
33
+ /**
34
+ * A class, not an interface, so it is a runtime value and can therefore be a
35
+ * constructor parameter type that `@dunx/transform` records - the same reason
36
+ * `QueueOptions` and `RedisOptions` are classes.
37
+ */
38
+ export declare class StaticOptions {
39
+ readonly root: string;
40
+ readonly path: string;
41
+ readonly maxAge: number;
42
+ readonly immutable: (pathname: string) => boolean;
43
+ constructor(init: StaticOptionsInit);
44
+ }
45
+ /** A leading slash and no trailing one, so `${path}/x` is never `//x`. */
46
+ export declare const normalizePrefix: (path: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -58,7 +58,7 @@
58
58
  "@dunx/core": "workspace:*"
59
59
  },
60
60
  "peerDependencies": {
61
- "@dunx/core": "^1.1.0",
61
+ "@dunx/core": "^1.2.1",
62
62
  "@types/bun": ">=1.3.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
@@ -1,40 +0,0 @@
1
- // @bun
2
- // src/server/status.ts
3
- var HttpStatusCode = Object.freeze({
4
- OK: 200,
5
- CREATED: 201,
6
- ACCEPTED: 202,
7
- NO_CONTENT: 204,
8
- MOVED_PERMANENTLY: 301,
9
- FOUND: 302,
10
- NOT_MODIFIED: 304,
11
- TEMPORARY_REDIRECT: 307,
12
- PERMANENT_REDIRECT: 308,
13
- BAD_REQUEST: 400,
14
- UNAUTHORIZED: 401,
15
- PAYMENT_REQUIRED: 402,
16
- FORBIDDEN: 403,
17
- NOT_FOUND: 404,
18
- METHOD_NOT_ALLOWED: 405,
19
- NOT_ACCEPTABLE: 406,
20
- REQUEST_TIMEOUT: 408,
21
- CONFLICT: 409,
22
- GONE: 410,
23
- PRECONDITION_FAILED: 412,
24
- PAYLOAD_TOO_LARGE: 413,
25
- URI_TOO_LONG: 414,
26
- UNSUPPORTED_MEDIA_TYPE: 415,
27
- IM_A_TEAPOT: 418,
28
- UNPROCESSABLE_ENTITY: 422,
29
- TOO_MANY_REQUESTS: 429,
30
- INTERNAL_SERVER_ERROR: 500,
31
- NOT_IMPLEMENTED: 501,
32
- BAD_GATEWAY: 502,
33
- SERVICE_UNAVAILABLE: 503,
34
- GATEWAY_TIMEOUT: 504
35
- });
36
-
37
- export { HttpStatusCode };
38
-
39
- //# debugId=881F02139124CAE264756E2164756E21
40
- //# sourceMappingURL=chunk-x80f562w.js.map