@dunx/http 2.5.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/service.d.ts +11 -22
- package/dist/client.d.ts +8 -2
- package/dist/client.js +14 -134
- package/dist/compression/compression.d.ts +7 -12
- package/dist/compression/options.d.ts +11 -23
- package/dist/health/indicators.d.ts +5 -10
- package/dist/index.d.ts +20 -21
- package/dist/index.js +119 -1449
- package/dist/internal.d.ts +36 -0
- package/dist/internal.js +89 -0
- package/dist/route/metadata.d.ts +6 -9
- package/dist/route/schema.d.ts +27 -55
- 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 +29 -101
- package/dist/server/routes.d.ts +7 -19
- package/dist/server/trace-context.d.ts +6 -10
- 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 +6 -2
- package/dist/chunk-jh7jk0bn.js +0 -164
|
@@ -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
|
+
};
|
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,11 +1,9 @@
|
|
|
1
1
|
import type { BunRequest } from 'bun';
|
|
2
2
|
import type { DefaultStatus, HttpMethod } from './marker.js';
|
|
3
3
|
/**
|
|
4
|
-
* Standard Schema v1, restated rather than depended on
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* dependencies. Zod 4, Valibot and ArkType already satisfy this shape, so any of
|
|
8
|
-
* 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.
|
|
9
7
|
*/
|
|
10
8
|
export interface StandardSchemaV1<In = unknown, Out = In> {
|
|
11
9
|
readonly '~standard': {
|
|
@@ -35,11 +33,8 @@ export interface StandardSchemaIssue {
|
|
|
35
33
|
/** The validated output of a schema - `InferOutput<typeof CreateNote>` is `Note`. */
|
|
36
34
|
export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
|
|
37
35
|
/**
|
|
38
|
-
* A JSON Schema, as JSON.
|
|
39
|
-
*
|
|
40
|
-
* Declared here rather than in `@dunx/openapi` because {@link RouteSchemas} names
|
|
41
|
-
* it and that package depends on this one, so this is the lowest common owner.
|
|
42
|
-
* `@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.
|
|
43
38
|
*/
|
|
44
39
|
export type JsonSchema = Readonly<Record<string, unknown>>;
|
|
45
40
|
/**
|
|
@@ -55,9 +50,7 @@ export interface RouteSchemas {
|
|
|
55
50
|
readonly status?: number;
|
|
56
51
|
/**
|
|
57
52
|
* What the route answers with, keyed by status code, in the same Standard
|
|
58
|
-
* Schema the request side takes
|
|
59
|
-
* hoists into `components/schemas` exactly as a request body does, and there is
|
|
60
|
-
* one contract for both directions.
|
|
53
|
+
* Schema the request side takes.
|
|
61
54
|
*
|
|
62
55
|
* ```ts
|
|
63
56
|
* const one = {
|
|
@@ -66,19 +59,9 @@ export interface RouteSchemas {
|
|
|
66
59
|
* } as const satisfies RouteSchemas;
|
|
67
60
|
* ```
|
|
68
61
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* instead: the verb decorators constrain it against the entry for the success
|
|
73
|
-
* status, so a handler answering with a different shape is a `TS1241` naming
|
|
74
|
-
* the mismatched property. See {@link Returns}. Nothing in the request path
|
|
75
|
-
* reads this key.
|
|
76
|
-
*
|
|
77
|
-
* A plain {@link JsonSchema} is accepted here too, and only here: a JSON Schema
|
|
78
|
-
* needs no conversion, so documenting a response costs no validator. `$id` names
|
|
79
|
-
* it, hoisting it into `components/schemas` the way `.meta({ id })` does for a
|
|
80
|
-
* zod schema. `body`, `query` and `params` still take a Standard Schema, because
|
|
81
|
-
* 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`.
|
|
82
65
|
*
|
|
83
66
|
* ```ts
|
|
84
67
|
* response: {
|
|
@@ -92,10 +75,8 @@ export interface RouteSchemas {
|
|
|
92
75
|
export type ResponseMap = Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
|
|
93
76
|
/**
|
|
94
77
|
* The handler's parameter type, derived from its own options object. It has to be
|
|
95
|
-
* written out
|
|
96
|
-
*
|
|
97
|
-
* (docs/architecture/constraints.md) - but every field type still comes from the schemas, so nothing
|
|
98
|
-
* 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).
|
|
99
80
|
*
|
|
100
81
|
* ```ts
|
|
101
82
|
* const createNote = { body: CreateNote, status: HttpStatusCode.CREATED } as const;
|
|
@@ -125,8 +106,8 @@ export type Input<O extends RouteSchemas> = {
|
|
|
125
106
|
} : unknown);
|
|
126
107
|
/**
|
|
127
108
|
* The status a handler's return type is held to: an explicit `options.status`,
|
|
128
|
-
* else the verb's default. Widened to `number` without `as const`, which
|
|
129
|
-
*
|
|
109
|
+
* else the verb's default. Widened to `number` without `as const`, which turns
|
|
110
|
+
* the check off rather than misapplying it.
|
|
130
111
|
*/
|
|
131
112
|
type SuccessStatus<O extends RouteSchemas, M extends HttpMethod> = O extends {
|
|
132
113
|
status: infer S extends number;
|
|
@@ -138,39 +119,30 @@ type SuccessStatus<O extends RouteSchemas, M extends HttpMethod> = O extends {
|
|
|
138
119
|
*/
|
|
139
120
|
type Declared<S> = [InferOutput<S>] extends [never] ? unknown : Serialised<InferOutput<S>>;
|
|
140
121
|
/**
|
|
141
|
-
* The declared shape as JSON will present it
|
|
142
|
-
* array
|
|
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.
|
|
143
126
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* something that must not be mutated, would fail against a document it satisfies.
|
|
147
|
-
* Mutability does not survive `Response.json`, so it is not part of the contract.
|
|
148
|
-
*
|
|
149
|
-
* Only arrays need the rewrite; TypeScript already ignores a property's `readonly`
|
|
150
|
-
* modifier when checking assignability. The object branch is how nested arrays are
|
|
151
|
-
* reached, and functions are returned untouched because mapping over one would
|
|
152
|
-
* discard its call signature.
|
|
127
|
+
* Only arrays need it. The object branch reaches nested ones; functions are
|
|
128
|
+
* returned untouched, since mapping over one discards its call signature.
|
|
153
129
|
*/
|
|
154
130
|
type Serialised<T> = T extends readonly (infer E)[] ? readonly Serialised<E>[] : T extends (...args: never[]) => unknown ? T : T extends object ? {
|
|
155
131
|
readonly [K in keyof T]: Serialised<T[K]>;
|
|
156
132
|
} : T;
|
|
157
133
|
/**
|
|
158
|
-
* What a handler may return, given its
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* (docs/architecture/constraints.md), and that cuts both ways: this is the return
|
|
162
|
-
* half of the same guarantee `Input<O>` gives the parameter. Declaring
|
|
163
|
-
* `response: { 200: User }` stops being documentation a handler can contradict.
|
|
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.
|
|
164
137
|
*
|
|
165
|
-
* `Response` is always allowed
|
|
166
|
-
*
|
|
167
|
-
* success status has no `response` entry.
|
|
138
|
+
* `Response` is always allowed, and so is a promise of either. Nothing is checked
|
|
139
|
+
* when the success status has no `response` entry.
|
|
168
140
|
*/
|
|
169
141
|
export type Returns<O extends RouteSchemas, M extends HttpMethod> = SuccessBody<O, M> | Response | Promise<SuccessBody<O, M> | Response>;
|
|
170
142
|
/**
|
|
171
|
-
* `infer R extends ResponseMap` is
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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`.
|
|
174
146
|
*/
|
|
175
147
|
type SuccessBody<O extends RouteSchemas, M extends HttpMethod> = O extends {
|
|
176
148
|
response: infer R extends ResponseMap;
|
|
@@ -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;
|
|
@@ -2,18 +2,13 @@ export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
|
2
2
|
/**
|
|
3
3
|
* The request id, and the only thing that decides a request has one.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* validation 400, a mapped 500 and every unmatched 404 went out with no id on
|
|
9
|
-
* them, which are the responses a caller most needs in order to find the log line
|
|
10
|
-
* the middleware just wrote.
|
|
5
|
+
* The logging middleware sets the header on a response it returns, and a failure
|
|
6
|
+
* is never one - the error mapper builds a fresh `Response` outside the chain. So
|
|
7
|
+
* a guard's 401, a validation 400 and every unmatched 404 went out with no id.
|
|
11
8
|
*
|
|
12
|
-
* Recorded against the request rather than threaded through the mapper,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* turned request logging off, or a path it told the middleware to ignore, is still
|
|
16
|
-
* answered without a header: nothing minted an id, so there is none to stamp.
|
|
9
|
+
* Recorded against the request rather than threaded through the mapper, which an
|
|
10
|
+
* app writes its own of. {@link stamp} reads back what {@link assign} recorded, so
|
|
11
|
+
* a path nothing minted an id for is still answered without a header.
|
|
17
12
|
*/
|
|
18
13
|
export declare class RequestIds {
|
|
19
14
|
/**
|