@dunx/http 2.4.0 → 3.0.0

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.
Files changed (40) hide show
  1. package/README.md +31 -909
  2. package/dist/chunk-25g22350.js +58 -0
  3. package/dist/chunk-ywdpxbkf.js +1573 -0
  4. package/dist/client/module.d.ts +2 -7
  5. package/dist/client/options.d.ts +12 -0
  6. package/dist/client/service.d.ts +11 -22
  7. package/dist/client.d.ts +8 -2
  8. package/dist/client.js +26 -137
  9. package/dist/compression/compression.d.ts +25 -0
  10. package/dist/compression/module.d.ts +20 -0
  11. package/dist/compression/negotiate.d.ts +12 -0
  12. package/dist/compression/options.d.ts +61 -0
  13. package/dist/health/indicators.d.ts +5 -10
  14. package/dist/index.d.ts +24 -20
  15. package/dist/index.js +325 -1366
  16. package/dist/internal.d.ts +36 -0
  17. package/dist/internal.js +89 -0
  18. package/dist/route/decorators.d.ts +6 -6
  19. package/dist/route/marker.d.ts +13 -0
  20. package/dist/route/metadata.d.ts +6 -9
  21. package/dist/route/schema.d.ts +59 -29
  22. package/dist/server/application.d.ts +33 -89
  23. package/dist/server/client-address.d.ts +6 -13
  24. package/dist/server/errors.d.ts +24 -55
  25. package/dist/server/request-id.d.ts +6 -11
  26. package/dist/server/request-logging.d.ts +33 -91
  27. package/dist/server/routes.d.ts +8 -20
  28. package/dist/server/trace-context.d.ts +47 -0
  29. package/dist/static/files.d.ts +5 -13
  30. package/dist/static/module.d.ts +10 -19
  31. package/dist/throttle/guard.d.ts +6 -9
  32. package/dist/throttle/module.d.ts +6 -8
  33. package/dist/throttle/store.d.ts +11 -20
  34. package/dist/ws/middleware.d.ts +12 -21
  35. package/dist/ws/redis-relay.d.ts +9 -19
  36. package/package.json +7 -3
  37. package/dist/chunk-sz4pvqxy.js +0 -111
  38. package/dist/chunk-sz4pvqxy.js.map +0 -10
  39. package/dist/client.js.map +0 -15
  40. package/dist/index.js.map +0 -53
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What the framework calls on itself. Every symbol here is still reachable from
3
+ * the package barrel, deprecated, and leaves it in 4.0.
4
+ *
5
+ * The barrel is a semver promise, and 173 of them was more than this package
6
+ * meant to make. What stays public is the surface an app writes against:
7
+ * decorators, options, contracts, errors, modules and the metadata helpers a
8
+ * user's own guard reads. What is here is route-table construction, the
9
+ * middleware fold, the relay codec and the discovery readers - things
10
+ * `@dunx/dashboard`, `@dunx/mcp` and `@dunx/openapi` need and an app does not.
11
+ *
12
+ * No stability promise attaches to this subpath.
13
+ */
14
+ export { discoverRoutes, joinPath, type DiscoveredRoute, } from './route/discover.js';
15
+ export { defaultStatusFor, type DefaultStatus, type RouteMeta, } from './route/marker.js';
16
+ export { guardsOf } from './route/metadata.js';
17
+ export { gatewaysOf, routesOf, type GatewayHandler, type GatewayNode, type RouteInputs, type RouteNode, } from './inspect.js';
18
+ export { buildContext } from './server/context.js';
19
+ export { preflight, withCors } from './server/cors.js';
20
+ export { isErrorFilter, toErrorMapper } from './server/errors.js';
21
+ export { compose } from './server/middleware.js';
22
+ export { assertNoCollisions, assertNoGatewayCollisions, buildRoutes, withUpgradeRoutes, type BunRoutes, type GuardResolver, type RouteMethod, type ServeRoutes, } from './server/routes.js';
23
+ export { normalizePrefix } from './static/options.js';
24
+ export { negotiate } from './compression/negotiate.js';
25
+ export { isCompressibleType } from './compression/options.js';
26
+ export { buildWebSocket, type UpgradeHandler, type WebSocketRuntime, } from './ws/adapter.js';
27
+ export { discoverGateway, discoverGateways, normalizePath, type DiscoveredGateway, type DiscoveredHandler, type Invoke, } from './ws/discover.js';
28
+ export { decode, encode } from './ws/envelope.js';
29
+ export { composeSocket, observe } from './ws/middleware.js';
30
+ export { HandlerKind, isGateway, type HandlerMeta } from './ws/marker.js';
31
+ export { defaultRelayUrl } from './ws/redis-relay.js';
32
+ export { decodeRelay, encodeRelay, type RelayFrame, type RelayPhase, } from './ws/relay.js';
33
+ export { buildGateways, buildRuntime, type GatewayRuntime, } from './ws/runtime.js';
34
+ export { HiddenHealthController } from './health/controller.js';
35
+ export { backoffDelay, executeWithRetry, isRetryableStatus, retryAfterMs, } from './client/retry.js';
36
+ export { isJsonBody, isPlainObject, safeStringify } from './client/json.js';
@@ -0,0 +1,89 @@
1
+ // @bun
2
+ import {
3
+ HandlerKind,
4
+ HiddenHealthController,
5
+ assertNoCollisions,
6
+ assertNoGatewayCollisions,
7
+ backoffDelay,
8
+ buildContext,
9
+ buildGateways,
10
+ buildRoutes,
11
+ buildRuntime,
12
+ buildWebSocket,
13
+ compose,
14
+ composeSocket,
15
+ decode,
16
+ decodeRelay,
17
+ defaultRelayUrl,
18
+ defaultStatusFor,
19
+ discoverGateway,
20
+ discoverGateways,
21
+ discoverRoutes,
22
+ encode,
23
+ encodeRelay,
24
+ executeWithRetry,
25
+ gatewaysOf,
26
+ guardsOf,
27
+ isCompressibleType,
28
+ isErrorFilter,
29
+ isGateway,
30
+ isJsonBody,
31
+ isPlainObject,
32
+ isRetryableStatus,
33
+ joinPath,
34
+ negotiate,
35
+ normalizePath,
36
+ normalizePrefix,
37
+ observe,
38
+ preflight,
39
+ retryAfterMs,
40
+ routesOf,
41
+ safeStringify,
42
+ toErrorMapper,
43
+ withCors,
44
+ withUpgradeRoutes
45
+ } from "./chunk-ywdpxbkf.js";
46
+ export {
47
+ HandlerKind,
48
+ HiddenHealthController,
49
+ assertNoCollisions,
50
+ assertNoGatewayCollisions,
51
+ backoffDelay,
52
+ buildContext,
53
+ buildGateways,
54
+ buildRoutes,
55
+ buildRuntime,
56
+ buildWebSocket,
57
+ compose,
58
+ composeSocket,
59
+ decode,
60
+ decodeRelay,
61
+ defaultRelayUrl,
62
+ defaultStatusFor,
63
+ discoverGateway,
64
+ discoverGateways,
65
+ discoverRoutes,
66
+ encode,
67
+ encodeRelay,
68
+ executeWithRetry,
69
+ gatewaysOf,
70
+ guardsOf,
71
+ isCompressibleType,
72
+ isErrorFilter,
73
+ isGateway,
74
+ isJsonBody,
75
+ isPlainObject,
76
+ isRetryableStatus,
77
+ joinPath,
78
+ negotiate,
79
+ normalizePath,
80
+ normalizePrefix,
81
+ observe,
82
+ preflight,
83
+ retryAfterMs,
84
+ routesOf,
85
+ safeStringify,
86
+ toErrorMapper,
87
+ withCors,
88
+ withUpgradeRoutes
89
+ };
@@ -1,10 +1,10 @@
1
1
  import { type RoutePath } from './marker.js';
2
- import type { Input, RouteSchemas } from './schema.js';
2
+ import type { Input, Returns, RouteSchemas } from './schema.js';
3
3
  type ControllerTarget = abstract new (...args: never[]) => object;
4
4
  export declare const Controller: (prefix?: string) => <T extends ControllerTarget>(target: T) => T;
5
- export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
6
- export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
7
- export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
8
- export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
9
- export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
5
+ export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "GET">>(value: H, _context: ClassMethodDecoratorContext) => H;
6
+ export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "POST">>(value: H, _context: ClassMethodDecoratorContext) => H;
7
+ export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PUT">>(value: H, _context: ClassMethodDecoratorContext) => H;
8
+ export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PATCH">>(value: H, _context: ClassMethodDecoratorContext) => H;
9
+ export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "DELETE">>(value: H, _context: ClassMethodDecoratorContext) => H;
10
10
  export {};
@@ -1,5 +1,18 @@
1
+ import { HttpStatusCode } from '../server/status.js';
1
2
  import type { RouteSchemas } from './schema.js';
2
3
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
4
+ /**
5
+ * The success status a route answers with when `options.status` is absent.
6
+ * `buildRoutes` and `@dunx/openapi`'s `statusOf` both read it, so the rule is
7
+ * stated once rather than in each of them.
8
+ */
9
+ export declare const defaultStatusFor: (method: HttpMethod) => number;
10
+ /**
11
+ * The type-level twin of {@link defaultStatusFor}, derived from the same constants
12
+ * so the two cannot drift. `Returns` needs it to know which `response` entry a
13
+ * handler is being held to.
14
+ */
15
+ export type DefaultStatus<M extends HttpMethod> = M extends 'POST' ? typeof HttpStatusCode.CREATED : typeof HttpStatusCode.OK;
3
16
  /**
4
17
  * A literal path, or a thunk read at **discovery** rather than at decoration.
5
18
  *
@@ -31,17 +31,14 @@ export declare const UNMATCHED: MetaKey<boolean>;
31
31
  export declare const Roles: (...roles: readonly string[]) => <F extends object>(target: F) => F;
32
32
  export declare const Public: () => <F extends object>(target: F) => F;
33
33
  /**
34
- * Route, but not documented. Valid on a method or on a class.
34
+ * Route, but not documented. Valid on a method or a class.
35
35
  *
36
- * The motivating case is a handler mounted on a wildcard: `@dunx/auth` routes
37
- * `<basePath>/*` to Better Auth's own handler, which is real and has to be
38
- * routed, but `*` is not an OpenAPI path template - so documenting it produced an
39
- * invalid entry named after an internal class, next to the 45 paths
40
- * `betterAuthDocument` describes properly.
36
+ * The motivating case is a wildcard mount: `@dunx/auth` routes `<basePath>/*` to
37
+ * better-auth's handler, and `*` is not an OpenAPI path template, so documenting
38
+ * it produced an invalid entry named after an internal class.
41
39
  *
42
- * It lives here rather than in `@dunx/openapi` because `@dunx/auth` must not
43
- * depend on the documentation package to say a route is undocumented, and this is
44
- * where the rest of the route metadata already is.
40
+ * Here rather than in `@dunx/openapi` so `@dunx/auth` need not depend on the
41
+ * documentation package to say a route is undocumented.
45
42
  */
46
43
  export declare const ApiHidden: () => <F extends object>(target: F) => F;
47
44
  /**
@@ -1,10 +1,9 @@
1
1
  import type { BunRequest } from 'bun';
2
+ import type { DefaultStatus, HttpMethod } from './marker.js';
2
3
  /**
3
- * Standard Schema v1, restated rather than depended on. The spec is an
4
- * *interface*, not a runtime: `@standard-schema/spec` ships nothing but these
5
- * declarations, so restating them costs one file and keeps `@dunx/http` at zero
6
- * dependencies. Zod 4, Valibot and ArkType already satisfy this shape, so any of
7
- * them drops straight into a route's options.
4
+ * Standard Schema v1, restated rather than depended on: the spec is an interface,
5
+ * so restating it keeps `@dunx/http` at zero dependencies. Zod 4, Valibot and
6
+ * ArkType all satisfy this shape already.
8
7
  */
9
8
  export interface StandardSchemaV1<In = unknown, Out = In> {
10
9
  readonly '~standard': {
@@ -34,11 +33,8 @@ export interface StandardSchemaIssue {
34
33
  /** The validated output of a schema - `InferOutput<typeof CreateNote>` is `Note`. */
35
34
  export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
36
35
  /**
37
- * A JSON Schema, as JSON. OpenAPI 3.1 embeds draft 2020-12 verbatim.
38
- *
39
- * Declared here rather than in `@dunx/openapi` because {@link RouteSchemas} names
40
- * it and that package depends on this one, so this is the lowest common owner.
41
- * `@dunx/openapi` re-exports it.
36
+ * A JSON Schema, as JSON. Declared here rather than in `@dunx/openapi` because
37
+ * {@link RouteSchemas} names it and that package depends on this one.
42
38
  */
43
39
  export type JsonSchema = Readonly<Record<string, unknown>>;
44
40
  /**
@@ -54,9 +50,7 @@ export interface RouteSchemas {
54
50
  readonly status?: number;
55
51
  /**
56
52
  * What the route answers with, keyed by status code, in the same Standard
57
- * Schema the request side takes - so a response schema with a `.meta({ id })`
58
- * hoists into `components/schemas` exactly as a request body does, and there is
59
- * one contract for both directions.
53
+ * Schema the request side takes.
60
54
  *
61
55
  * ```ts
62
56
  * const one = {
@@ -65,17 +59,9 @@ export interface RouteSchemas {
65
59
  * } as const satisfies RouteSchemas;
66
60
  * ```
67
61
  *
68
- * **Never validated.** It documents the response; it does not enforce it.
69
- * Running a validation pass over every response body would be a per-request
70
- * cost paid for a documentation feature, which is the wrong trade - the
71
- * handler's own return type is what checks the answer, at compile time and for
72
- * free. Nothing in the request path reads this key.
73
- *
74
- * A plain {@link JsonSchema} is accepted here too, and only here: a JSON Schema
75
- * needs no conversion, so documenting a response costs no validator. `$id` names
76
- * it, hoisting it into `components/schemas` the way `.meta({ id })` does for a
77
- * zod schema. `body`, `query` and `params` still take a Standard Schema, because
78
- * those are parsed.
62
+ * Never validated at runtime, checked at compile time against the success
63
+ * status. A plain {@link JsonSchema} is accepted here and only here, and `$id`
64
+ * hoists it into `components/schemas`.
79
65
  *
80
66
  * ```ts
81
67
  * response: {
@@ -83,14 +69,14 @@ export interface RouteSchemas {
83
69
  * }
84
70
  * ```
85
71
  */
86
- readonly response?: Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
72
+ readonly response?: ResponseMap;
87
73
  }
74
+ /** `response` keyed by status code. Named so {@link Returns} can constrain it. */
75
+ export type ResponseMap = Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
88
76
  /**
89
77
  * The handler's parameter type, derived from its own options object. It has to be
90
- * written out - a standard method decorator can *check* a parameter's type but
91
- * cannot contextually type an unannotated one
92
- * (docs/architecture/constraints.md) - but every field type still comes from the schemas, so nothing
93
- * is declared twice:
78
+ * written out: a standard method decorator can check a parameter's type but not
79
+ * contextually type an unannotated one (docs/architecture/constraints.md).
94
80
  *
95
81
  * ```ts
96
82
  * const createNote = { body: CreateNote, status: HttpStatusCode.CREATED } as const;
@@ -118,6 +104,49 @@ export type Input<O extends RouteSchemas> = {
118
104
  } ? {
119
105
  readonly params: InferOutput<P>;
120
106
  } : unknown);
107
+ /**
108
+ * The status a handler's return type is held to: an explicit `options.status`,
109
+ * else the verb's default. Widened to `number` without `as const`, which turns
110
+ * the check off rather than misapplying it.
111
+ */
112
+ type SuccessStatus<O extends RouteSchemas, M extends HttpMethod> = O extends {
113
+ status: infer S extends number;
114
+ } ? S : DefaultStatus<M>;
115
+ /**
116
+ * A plain {@link JsonSchema} carries no type to infer, so it becomes `unknown` and
117
+ * absorbs whatever the handler returns. That is the escape hatch for a response
118
+ * whose shape no schema value describes.
119
+ */
120
+ type Declared<S> = [InferOutput<S>] extends [never] ? unknown : Serialised<InferOutput<S>>;
121
+ /**
122
+ * The declared shape as JSON will present it: the same shape with every array made
123
+ * readonly. `z.array()` infers a mutable `T[]`, so a method correctly returning
124
+ * `readonly User[]` would fail against a document it satisfies, and mutability
125
+ * does not survive `Response.json` anyway.
126
+ *
127
+ * Only arrays need it. The object branch reaches nested ones; functions are
128
+ * returned untouched, since mapping over one discards its call signature.
129
+ */
130
+ type Serialised<T> = T extends readonly (infer E)[] ? readonly Serialised<E>[] : T extends (...args: never[]) => unknown ? T : T extends object ? {
131
+ readonly [K in keyof T]: Serialised<T[K]>;
132
+ } : T;
133
+ /**
134
+ * What a handler may return, given its options object and its verb. The return
135
+ * half of the guarantee `Input<O>` gives the parameter, so `response: { 200: User }`
136
+ * stops being documentation a handler can contradict.
137
+ *
138
+ * `Response` is always allowed, and so is a promise of either. Nothing is checked
139
+ * when the success status has no `response` entry.
140
+ */
141
+ export type Returns<O extends RouteSchemas, M extends HttpMethod> = SuccessBody<O, M> | Response | Promise<SuccessBody<O, M> | Response>;
142
+ /**
143
+ * `infer R extends ResponseMap` is required: without it the narrowed `O` is
144
+ * `{ response: R } & O`, whose `response` no longer satisfies `RouteSchemas`, and
145
+ * `SuccessStatus<O, M>` fails with `TS2344`.
146
+ */
147
+ type SuccessBody<O extends RouteSchemas, M extends HttpMethod> = O extends {
148
+ response: infer R extends ResponseMap;
149
+ } ? SuccessStatus<O, M> extends keyof R ? Declared<R[SuccessStatus<O, M>]> : unknown : unknown;
121
150
  /** What the framework actually hands a handler; `Input<O>` is its typed view. */
122
151
  export interface RouteInput {
123
152
  readonly req: BunRequest;
@@ -125,3 +154,4 @@ export interface RouteInput {
125
154
  readonly query?: unknown;
126
155
  readonly params?: unknown;
127
156
  }
157
+ export {};
@@ -16,111 +16,62 @@ export interface HttpOptions extends AppOptions {
16
16
  /** Resolved from the container, so middleware can inject(). */
17
17
  readonly middleware?: readonly Ctor<Middleware>[];
18
18
  /**
19
- * Replaces the default mapper.
20
- *
21
- * A bare `ErrorMapper` function, or an `ErrorFilter` **class** - which is the one
22
- * to prefer, because a class is resolved from the container and can therefore
23
- * inject the `Logger` or the config a real filter needs. A mapper cannot; dunx's
24
- * own default has to be curried over its logger for exactly that reason.
25
- *
26
- * A filter with dependencies needs them bindable, the same rule `middleware`
27
- * entries follow; one with none self-binds and needs no `providers` entry.
19
+ * Replaces the default mapper. Prefer an `ErrorFilter` class over a bare
20
+ * `ErrorMapper`: a class is resolved from the container and can inject.
28
21
  */
29
22
  readonly onError?: ErrorHandler;
30
23
  /**
31
- * One structured entry per request, on by default. `false` removes it; an
32
- * options object tunes what it records. See {@link RequestLoggingMiddleware}.
33
- *
34
- * It is the **outermost** middleware, ahead of anything `middleware` declares,
35
- * so a request rejected by a guard is still logged with the status it got.
24
+ * One structured entry per request, on by default and outermost, so a request
25
+ * a guard rejected is still logged with the status it got.
26
+ * See {@link RequestLoggingMiddleware}.
36
27
  */
37
28
  readonly requestLogging?: boolean | RequestLoggingOptions;
38
29
  /**
39
- * One entry at `listen()` naming every route and gateway the process serves. On
40
- * by default, because it is the answer to "is my route registered" and a service
41
- * that logs nothing at boot cannot answer it from production.
42
- *
43
- * `false` removes it. Separate from `requestLogging` rather than sharing its
44
- * switch: one is per request and one is per process, and silencing the noisy one
45
- * is not a reason to lose the quiet one. `@dunx/testing` defaults it off, for the
46
- * same reason it defaults request logging off.
30
+ * One entry at `listen()` naming every route and gateway served. On by default,
31
+ * and switched separately from `requestLogging`: one is per process, the other
32
+ * per request. `@dunx/testing` defaults it off.
47
33
  */
48
34
  readonly bootLogging?: boolean;
49
- /**
50
- * Bun's `websocket` options, plus where a throwing handler goes. Server-wide, so
51
- * they live here next to `middleware` rather than on a module: gateways
52
- * themselves are declared in `@Module({ providers })`.
53
- */
35
+ /** Bun's `websocket` options, plus where a throwing handler goes. Server-wide;
36
+ * gateways themselves are declared in `@Module({ providers })`. */
54
37
  readonly websocket?: SocketOptions;
55
38
  /**
56
- * The socket half of `middleware`, resolved from the container the same way.
57
- *
58
- * Each entry wraps every dispatched gateway handler - open, each named message,
59
- * the catch-all, close, drain, ping and pong - the way an HTTP middleware wraps a
60
- * route. `socketLogging`'s middleware runs outermost, ahead of anything here.
39
+ * The socket half of `middleware`. Each entry wraps every dispatched gateway
40
+ * handler; `socketLogging`'s runs outermost, ahead of anything here.
61
41
  */
62
42
  readonly socketMiddleware?: readonly Ctor<SocketMiddleware>[];
63
43
  /**
64
- * One structured entry per socket frame, on by default at **`debug`**. `false`
65
- * removes it; an options object tunes the level per event. See
66
- * {@link SocketLoggingMiddleware}.
67
- *
68
- * `debug` rather than request logging's `info`, because a gateway can take a
69
- * frame per connection per tick. The default `ConsoleLogger` threshold is
70
- * `info`, so this writes nothing until an app lowers its level or names a louder
71
- * one here.
72
- *
73
- * Installing it also takes `SocketOptions.onError`'s `console.error` default out
74
- * of the way: a middleware wraps the handler, so the failure is already reported
75
- * through the `Logger` with the gateway and the event on it.
44
+ * One structured entry per socket frame, on by default at `debug` - a gateway
45
+ * can take a frame per connection per tick, so it writes nothing until an app
46
+ * lowers its level. See {@link SocketLoggingMiddleware}.
76
47
  */
77
48
  readonly socketLogging?: boolean | SocketLoggingOptions;
78
49
  /**
79
- * Multi-node websocket fan-out. Absent - the default - means `PubSub` publishes
80
- * to this process only, which is exactly Bun's native pub/sub and costs nothing.
81
- *
82
- * `new RedisRelay({ url })` is the batteries-included one. Anything with a
83
- * `publish` and a `subscribe` fits, including `@dunx/infra`'s `RedisConnection`,
84
- * which has to come out of the container and so goes through
85
- * `app.get(PubSub).relayThrough(...)` instead of this option.
50
+ * Multi-node websocket fan-out. Absent means `PubSub` publishes to this process
51
+ * only. Anything with `publish` and `subscribe` fits; one that has to come out
52
+ * of the container goes through `app.get(PubSub).relayThrough(...)` instead.
86
53
  */
87
54
  readonly relay?: PubSubRelay;
88
55
  /** The broker channel the relay carries frames on. @default 'dunx:ws' */
89
56
  readonly relayChannel?: string;
90
57
  /**
91
- * How hard to retry a subscribe that failed. Same shape as
92
- * `RelayOptions.resubscribe`: bounded, doubling, and on an unref'd timer, so a
93
- * broker that never comes back cannot hold the process open.
94
- *
95
- * Here rather than only on `relayThrough` because reaching for that to set one
96
- * option means giving up `relay` above entirely - the two conflict, and the
97
- * second to run throws `PubSub already relays`.
58
+ * How hard to retry a failed subscribe. Bounded, doubling, on an unref'd timer,
59
+ * so a broker that never returns cannot hold the process open.
98
60
  */
99
61
  readonly relayResubscribe?: RelayOptions['resubscribe'];
100
62
  /**
101
- * What an unmatched path looks like to global middleware.
102
- *
103
- * `'guarded'`, the default, gives the miss no route metadata, so a global guard
104
- * refuses it and an anonymous caller gets that guard's status rather than a 404.
105
- * That is deliberate: a 404 on a miss while every real path answers 401 tells a
106
- * prober which paths exist.
107
- *
108
- * `'public'` reports the miss as `@Public()`, so a guard honouring that flag
109
- * passes it through to the conventional 404. The request is still logged and
110
- * still gets a request id either way, which is the whole reason the fallback
111
- * runs the middleware at all.
112
- *
113
- * A guard can discriminate under either setting: `UNMATCHED` is set on the miss
114
- * and no real route ever sets it.
63
+ * What an unmatched path looks like to global middleware. `'guarded'` gives the
64
+ * miss no route metadata, so a global guard refuses it and a prober cannot tell
65
+ * a 404 from a 401. `'public'` reports it as `@Public()` for a conventional 404.
66
+ * Either way `UNMATCHED` is set, which no real route sets.
115
67
  *
116
68
  * @default 'guarded'
117
69
  */
118
70
  readonly notFound?: 'guarded' | 'public';
119
71
  }
120
72
  /**
121
- * Everything below `listen()` configures the route table, which is built exactly
122
- * once - when the server binds. Calling any of them afterwards throws rather than
123
- * being quietly dropped.
73
+ * Everything below `listen()` configures the route table, built once when the
74
+ * server binds. Calling any of them afterwards throws.
124
75
  */
125
76
  export interface HttpApp extends App {
126
77
  /** Prefixes every discovered route. Last call wins. */
@@ -151,23 +102,16 @@ export declare class HttpApplication implements HttpApp {
151
102
  setting<K extends keyof AppSettings>(key: K): AppSettings[K];
152
103
  enableCors(options?: CorsOptions): this;
153
104
  clientIp(req: BunRequest): string | undefined;
154
- /**
155
- * The one `Bun.serve` call. A gateway's upgrade is a native `GET` route in the
156
- * same table, so Bun's router - not a hand-written `fetch` fallback - is what
157
- * matches an upgrade, and no `fetch` handler is needed at all.
158
- */
105
+ /** The one `Bun.serve` call. A gateway's upgrade is a native `GET` route in the
106
+ * same table, so Bun's router matches it and no `fetch` handler is needed. */
159
107
  listen(port?: number): Promise<string>;
160
- /**
161
- * Delegated unchanged: the drain is the container's phase, and `shutdown()`
162
- * runs it. Public so an operator can start draining without committing to a
163
- * shutdown, which is what a readiness probe wants during a rolling deploy.
164
- */
108
+ /** Public so an operator can start draining without committing to a shutdown,
109
+ * which is what a readiness probe wants during a rolling deploy. */
165
110
  drain(): Promise<void>;
166
111
  /**
167
- * The four phases, in order, and **none of them is skipped because an earlier
168
- * one failed**. A drain hook that threw used to abort this before `server.stop()`
169
- * had run, so the port stayed open and `closed` never resolved; each failure is
170
- * collected now and thrown once the whole teardown is over.
112
+ * Four phases in order, none skipped because an earlier one failed. A throwing
113
+ * drain hook used to abort before `server.stop()`, leaving the port open and
114
+ * `closed` unresolved; failures are collected and thrown at the end.
171
115
  */
172
116
  shutdown(): Promise<void>;
173
117
  enableShutdownHooks(signals?: readonly ShutdownSignal[], options?: ShutdownHookOptions): this;
@@ -4,20 +4,13 @@ export interface AddressSource {
4
4
  readonly trustProxy: boolean | number;
5
5
  }
6
6
  /**
7
- * The client's address, honouring the `'trust proxy'` setting.
7
+ * The client's address, honouring `'trust proxy'`. The address is counted from the
8
+ * right of `X-Forwarded-For` by the number of trusted hops, never from the left: a
9
+ * client can send anything, and only the entries a proxy appended carry weight.
8
10
  *
9
- * With the setting on, the address is read from `X-Forwarded-For` counting from
10
- * the right by the number of trusted hops, never from the left. A client can put
11
- * anything in the header it sends; only the entries a proxy appended carry any
12
- * weight, and there are exactly as many of those as there are proxies in front of
13
- * this server.
14
- *
15
- * Bound and exported by `HttpFactory`'s global wrapper module, so injecting it in a
16
- * middleware or controller needs no registration and `app.clientIp(req)` is the same
17
- * instance. That binding is not optional under module scoping: an unbound class
18
- * self-binds into whichever scope asks first, so a second module injecting it was a
19
- * boot error naming the first, and `listen()` could attach the server to an instance
20
- * nothing else held.
11
+ * Bound by `HttpFactory`'s global wrapper, which is not optional under module
12
+ * scoping - an unbound class self-binds into whichever scope asks first, so a
13
+ * second module injecting it was a boot error.
21
14
  */
22
15
  export declare class ClientAddress {
23
16
  of(req: BunRequest): string | undefined;
@@ -1,14 +1,9 @@
1
1
  import { AppError, type Ctor, type Logger } from '@dunx/core';
2
2
  export interface HttpErrorOptions extends ErrorOptions {
3
3
  /**
4
- * Headers the error response carries. `Retry-After` on a 429,
5
- * `WWW-Authenticate` on a 401, `Allow` on a 405 - each of them part of the
6
- * status rather than an extra, and none of them expressible by a throw before
7
- * this existed.
8
- *
9
- * {@link errorMapper} copies them onto the response. An app that replaces the
10
- * mapper has to read them itself, which is the same contract `status` and
11
- * `message` already have.
4
+ * Headers the error response carries: `Retry-After` on a 429,
5
+ * `WWW-Authenticate` on a 401, `Allow` on a 405. {@link errorMapper} copies them
6
+ * onto the response; an app replacing the mapper reads them itself.
12
7
  */
13
8
  readonly headers?: Readonly<Record<string, string>>;
14
9
  }
@@ -37,16 +32,9 @@ export declare class ValidationError extends HttpError {
37
32
  }
38
33
  export type ErrorMapper = (error: unknown, req: Request) => Response;
39
34
  /**
40
- * The class form of {@link ErrorMapper}, and the one to reach for in an app.
41
- *
42
- * A mapper is a function, which means it cannot inject: the interesting ones need
43
- * the app's config to decide how much of an error to reveal, or its `Logger` to
44
- * record the ones that became a 500. dunx's own default proves the point - it is
45
- * `errorMapper(logger)`, a curried factory, because currying was the only way to
46
- * hand a function a dependency.
47
- *
48
- * A filter is resolved **from the container**, exactly as `HttpOptions.middleware`
49
- * entries are, so it takes whatever it needs as constructor parameters:
35
+ * The class form of {@link ErrorMapper}, and the one to reach for in an app. A
36
+ * mapper is a function and cannot inject; a filter is resolved from the container
37
+ * exactly as `HttpOptions.middleware` entries are:
50
38
  *
51
39
  * ```ts
52
40
  * export class AppErrorFilter extends ErrorFilter {
@@ -66,60 +54,41 @@ export type ErrorMapper = (error: unknown, req: Request) => Response;
66
54
  * HttpFactory.create(root, { onError: AppErrorFilter });
67
55
  * ```
68
56
  *
69
- * `abstract class` rather than an interface, so it is a runtime value and therefore
70
- * usable as an injection token - an app that wants to swap filters by binding one
71
- * can. Extending it is optional: `onError` accepts any class with a matching
72
- * `catch`, because the check is structural.
73
- *
74
- * The method is `catch` to match the vocabulary of the thing it replaces, NestJS's
75
- * `ExceptionFilter.catch`. A filter that cannot handle an error should rethrow it,
76
- * or delegate to `defaultErrorMapper`.
57
+ * `abstract class` rather than an interface, so it is a runtime value and usable
58
+ * as an injection token. Extending it is optional: the check is structural. A
59
+ * filter that cannot handle an error should rethrow or delegate to
60
+ * `defaultErrorMapper`.
77
61
  */
78
62
  export declare abstract class ErrorFilter {
79
63
  abstract catch(error: unknown, req: Request): Response;
80
64
  }
81
- /**
82
- * What `onError` accepts. A bare mapper still works and is the cheaper thing for a
83
- * filter with no dependencies; a class is what an app that needs one uses.
84
- */
65
+ /** What `onError` accepts. A bare mapper is cheaper where nothing is injected. */
85
66
  export type ErrorHandler = ErrorMapper | Ctor<ErrorFilter>;
86
67
  /**
87
- * Whether `onError` was given a class rather than a mapper.
88
- *
89
- * Both are `typeof === 'function'`, so the discriminator is the prototype carrying
90
- * a `catch`: a class declaration always has one, and neither an arrow function nor
91
- * a `function` expression ever does. Checking `prototype` alone would be wrong -
92
- * `function mapper() {}` has an empty one.
68
+ * Whether `onError` was given a class rather than a mapper. Both are functions, so
69
+ * the discriminator is a prototype carrying a `catch`. `prototype` alone would be
70
+ * wrong: `function mapper() {}` has an empty one.
93
71
  */
94
72
  export declare const isErrorFilter: (handler: ErrorHandler) => handler is Ctor<ErrorFilter>;
95
73
  /**
96
- * Narrows an `ErrorHandler` to the mapper the request path actually calls.
97
- *
98
- * `resolve` is typed for this one token rather than generically: the only thing ever
99
- * looked up here is the filter, and a `<T>(token: Ctor<T>) => T` signature makes
100
- * every caller - a test included - satisfy a polymorphic contract it does not need.
74
+ * Narrows an `ErrorHandler` to the mapper the request path calls. `resolve` is
75
+ * typed for this one token rather than generically, so no caller has to satisfy a
76
+ * polymorphic contract it does not need.
101
77
  */
102
78
  export declare const toErrorMapper: (handler: ErrorHandler, resolve: (token: Ctor<ErrorFilter>) => ErrorFilter) => ErrorMapper;
103
79
  /**
104
80
  * The mapper `HttpFactory` installs unless `onError` replaces it, built from the
105
- * app's **bound** `Logger` - so a service that imported `@dunx/infra/logger` gets
106
- * the stack as one `@arkv/logger` entry, sanitized and shaped like every other.
107
- *
108
- * An `HttpError` is not logged here at all: the status is the whole record, and
109
- * `RequestLoggingMiddleware` already writes the 4xx line. Only an error nothing
110
- * declared - the one that becomes a 500 - is worth a stack.
81
+ * app's bound `Logger`. An `HttpError` is not logged here: the status is the whole
82
+ * record and `RequestLoggingMiddleware` already wrote the line, so only an
83
+ * undeclared error is worth a stack.
111
84
  *
112
- * The error goes in as its own argument rather than as a field of an object.
113
- * `JSON.stringify(new Error('x'))` is `{}`, so `{ err: error }` would drop the
114
- * stack; every `Logger` implementation picks an `Error` argument out and
115
- * serialises it.
85
+ * The error is its own argument, not a field: `JSON.stringify(new Error('x'))` is
86
+ * `{}`, so `{ err: error }` would drop the stack.
116
87
  */
117
88
  export declare const errorMapper: (logger: Logger) => ErrorMapper;
118
89
  /**
119
90
  * The same mapper with no container behind it, for `buildRoutes` and
120
- * `buildFallback` called directly. It writes through core's `ConsoleLogger`, which
121
- * is one JSON line - the point being that nothing in this package ever reaches for
122
- * `console.error` and emits a multi-line dump a collector reads as several broken
123
- * records. An app gets {@link errorMapper} over its own bound logger instead.
91
+ * `buildFallback` called directly. Writes through core's `ConsoleLogger`, so
92
+ * nothing here emits a multi-line dump a collector reads as several records.
124
93
  */
125
94
  export declare const defaultErrorMapper: ErrorMapper;