@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.
- package/README.md +31 -909
- package/dist/chunk-25g22350.js +58 -0
- package/dist/chunk-ywdpxbkf.js +1573 -0
- package/dist/client/module.d.ts +2 -7
- package/dist/client/options.d.ts +12 -0
- package/dist/client/service.d.ts +11 -22
- package/dist/client.d.ts +8 -2
- package/dist/client.js +26 -137
- package/dist/compression/compression.d.ts +25 -0
- package/dist/compression/module.d.ts +20 -0
- package/dist/compression/negotiate.d.ts +12 -0
- package/dist/compression/options.d.ts +61 -0
- package/dist/health/indicators.d.ts +5 -10
- package/dist/index.d.ts +24 -20
- package/dist/index.js +325 -1366
- package/dist/internal.d.ts +36 -0
- package/dist/internal.js +89 -0
- package/dist/route/decorators.d.ts +6 -6
- package/dist/route/marker.d.ts +13 -0
- package/dist/route/metadata.d.ts +6 -9
- package/dist/route/schema.d.ts +59 -29
- package/dist/server/application.d.ts +33 -89
- package/dist/server/client-address.d.ts +6 -13
- package/dist/server/errors.d.ts +24 -55
- package/dist/server/request-id.d.ts +6 -11
- package/dist/server/request-logging.d.ts +33 -91
- package/dist/server/routes.d.ts +8 -20
- package/dist/server/trace-context.d.ts +47 -0
- package/dist/static/files.d.ts +5 -13
- package/dist/static/module.d.ts +10 -19
- package/dist/throttle/guard.d.ts +6 -9
- package/dist/throttle/module.d.ts +6 -8
- package/dist/throttle/store.d.ts +11 -20
- package/dist/ws/middleware.d.ts +12 -21
- package/dist/ws/redis-relay.d.ts +9 -19
- package/package.json +7 -3
- package/dist/chunk-sz4pvqxy.js +0 -111
- package/dist/chunk-sz4pvqxy.js.map +0 -10
- package/dist/client.js.map +0 -15
- 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';
|
package/dist/internal.js
ADDED
|
@@ -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) => <
|
|
6
|
-
export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <
|
|
7
|
-
export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <
|
|
8
|
-
export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <
|
|
9
|
-
export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <
|
|
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 {};
|
package/dist/route/marker.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/route/metadata.d.ts
CHANGED
|
@@ -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
|
|
34
|
+
* Route, but not documented. Valid on a method or a class.
|
|
35
35
|
*
|
|
36
|
-
* The motivating case is a
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
/**
|
package/dist/route/schema.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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?:
|
|
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
|
|
91
|
-
*
|
|
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
|
|
32
|
-
*
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
65
|
-
*
|
|
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
|
|
80
|
-
*
|
|
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
|
|
92
|
-
*
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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,
|
|
122
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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;
|
package/dist/server/errors.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
* `WWW-Authenticate` on a 401, `Allow` on a 405
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
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
|
-
*
|
|
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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
|
106
|
-
*
|
|
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
|
|
113
|
-
* `
|
|
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.
|
|
121
|
-
*
|
|
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;
|