@dunx/http 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -909
- package/dist/chunk-25g22350.js +58 -0
- package/dist/chunk-b699cfes.js +130 -0
- package/dist/chunk-f6pw36av.js +1299 -0
- package/dist/{chunk-jh7jk0bn.js → chunk-sz4pvqxy.js} +1 -57
- package/dist/client/module.d.ts +2 -7
- package/dist/client/service.d.ts +11 -22
- package/dist/client.d.ts +1 -2
- package/dist/client.js +12 -142
- 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 +13 -21
- package/dist/index.js +102 -1484
- package/dist/internal.d.ts +36 -0
- package/dist/internal.js +154 -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/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
|
/**
|
|
@@ -6,57 +6,26 @@ export interface RequestLoggingOptions {
|
|
|
6
6
|
/** Bodies past this many characters are logged as a size. Default 2048. `0` omits them. */
|
|
7
7
|
readonly maxBodyLength?: number;
|
|
8
8
|
/**
|
|
9
|
-
* Log the request body. Default
|
|
9
|
+
* Log the request body. Default `false`, and the cost depends on whether the
|
|
10
|
+
* route declares a `body` schema: +1.9 us when it does, +28.8 us when it does
|
|
11
|
+
* not, because the logger has to `req.clone()` an unread network stream.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* a factor of fifteen. `bun run logging:bodies` in `internal/bench`, round-robin
|
|
13
|
-
* over 3 runs against `POST /validate`:
|
|
14
|
-
*
|
|
15
|
-
* | setting | us/req | vs the default |
|
|
16
|
-
* | ---------------------------------------- | -----: | -------------: |
|
|
17
|
-
* | `requestLogging: false` | 12.80 | -4.45 us |
|
|
18
|
-
* | the shipped default, both bodies off | 17.25 | - |
|
|
19
|
-
* | **`requestBody: true`, schema route** | **19.12** | **+1.87 us** |
|
|
20
|
-
* | `responseBody: true` | 19.80 | +2.55 us |
|
|
21
|
-
* | both bodies, schema route | 20.03 | +2.78 us |
|
|
22
|
-
* | **`requestBody: true`, no schema** | **46.06** | **+28.81 us** |
|
|
23
|
-
*
|
|
24
|
-
* A route with a schema has already had its body buffered by the input reader, so
|
|
25
|
-
* the logger reads that text and nothing is cloned. A route without one leaves the
|
|
26
|
-
* logger to `req.clone()`, and cloning a request whose body is an unread network
|
|
27
|
-
* stream is the entire cost - not the second `JSON.parse`, which is 0.32 us.
|
|
28
|
-
* `raw-body.ts` has that decomposition.
|
|
29
|
-
*
|
|
30
|
-
* It is the field most likely to contain a password. Turn it on in development,
|
|
31
|
-
* where seeing the payload is the point.
|
|
13
|
+
* It is the field most likely to contain a password.
|
|
32
14
|
*/
|
|
33
15
|
readonly requestBody?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* No equivalent trick here and none needed: a response is already a materialised
|
|
38
|
-
* string by the time this clones it, which is why it was never the expensive half.
|
|
39
|
-
*/
|
|
16
|
+
/** Log the response body. Default `false`, +2.6 us. A response is already a
|
|
17
|
+
* materialised string by the time this clones it. */
|
|
40
18
|
readonly responseBody?: boolean;
|
|
41
19
|
/**
|
|
42
|
-
* Paths to skip entirely
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated. That
|
|
46
|
-
* is what makes it free. `correlateIgnored` buys the correlation back.
|
|
20
|
+
* Paths to skip entirely: no entry, no `x-request-id`, and no
|
|
21
|
+
* `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated.
|
|
22
|
+
* `correlateIgnored` buys the correlation back.
|
|
47
23
|
*/
|
|
48
24
|
readonly ignore?: readonly string[];
|
|
49
25
|
/**
|
|
50
|
-
* Path
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* and a health check is one path. A mount is not: `@dunx/dashboard` at
|
|
54
|
-
* `/_dunx` polls four endpoints every five seconds and bull-board pulls a
|
|
55
|
-
* dozen assets, and listing them is both tedious and wrong the moment either
|
|
56
|
-
* grows an endpoint.
|
|
57
|
-
*
|
|
58
|
-
* Scanned only when non-empty, so an app that sets none pays nothing - the
|
|
59
|
-
* same guard `ignore` has. Keep the list short; it is a loop.
|
|
26
|
+
* Path prefixes to skip, for a whole mount rather than one path. `ignore` is an
|
|
27
|
+
* exact-match `Set`; this is a loop, so keep the list short. Scanned only when
|
|
28
|
+
* non-empty.
|
|
60
29
|
*
|
|
61
30
|
* ```ts
|
|
62
31
|
* requestLogging: { ignorePrefix: ['/_dunx'] }
|
|
@@ -65,78 +34,37 @@ export interface RequestLoggingOptions {
|
|
|
65
34
|
readonly ignorePrefix?: readonly string[];
|
|
66
35
|
/**
|
|
67
36
|
* Keep the request id and the async scope on an `ignore`d path. Default
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* still writes no entry of its own; it gets an id - inbound or minted - on the
|
|
72
|
-
* response, and everything the handler logs carries it.
|
|
73
|
-
*
|
|
74
|
-
* It is not the default because it is not free: the ignored path pays for
|
|
75
|
-
* reading the header, `crypto.randomUUID()`, the `runWithContext` scope and the
|
|
76
|
-
* response header. On the `bun run logging` decomposition those four rows are
|
|
77
|
-
* ~2.2 µs, against ~5.4 µs for the whole default path - so it costs the half
|
|
78
|
-
* that buys correlation and not the half that builds and serialises the entry.
|
|
37
|
+
* `false`. The path still writes no entry; it gets an id on the response and
|
|
38
|
+
* everything the handler logs carries it. Costs ~2.2 us of the ~5.4 us the
|
|
39
|
+
* default path spends.
|
|
79
40
|
*/
|
|
80
41
|
readonly correlateIgnored?: boolean;
|
|
81
42
|
/**
|
|
82
|
-
* Wrap every request in an `AsyncLocalStorage` scope. Default
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* `runWithContext` row of `bun run logging` is **+0.91 µs**, 17% of the 5.38 µs
|
|
87
|
-
* request logging costs over `requestLogging: false`.
|
|
88
|
-
*
|
|
89
|
-
* `correlate: false` skips it. **The request entry is unchanged** - the same
|
|
90
|
-
* `requestId`, `method`, `event`, `flow` and `context` fields are written onto
|
|
91
|
-
* it directly instead of being read back out of the store. What is lost is
|
|
92
|
-
* everything *else* the request logs: those lines carry no `requestId`, and
|
|
93
|
-
* `updateContext` from a handler has nothing to update.
|
|
94
|
-
*
|
|
95
|
-
* Worth it for an app whose handlers never log, or one that passes correlation
|
|
96
|
-
* explicitly. Leave it on otherwise; correlation is most of what a request id
|
|
97
|
-
* is for.
|
|
43
|
+
* Wrap every request in an `AsyncLocalStorage` scope. Default `true`, +0.91 us.
|
|
44
|
+
* It is what lets a service four frames down log `requestId` without being
|
|
45
|
+
* handed a request. `correlate: false` skips it; this middleware's own entry is
|
|
46
|
+
* unchanged, but every other line the request writes loses its id.
|
|
98
47
|
*/
|
|
99
48
|
readonly correlate?: boolean;
|
|
100
49
|
/**
|
|
101
50
|
* Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
|
|
102
|
-
* `requestId`
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* span of the caller's; off, nothing reads the header and nothing is minted.
|
|
106
|
-
* It costs a header read and 8 random bytes per request, which is not worth
|
|
107
|
-
* paying in a service with nothing to correlate against - and `requestId`
|
|
108
|
-
* already spans two dunx services on its own.
|
|
109
|
-
*
|
|
110
|
-
* `@dunx/http/client` sends the adopted trace upstream, so turning this on at
|
|
111
|
-
* both ends is what makes one trace cover both.
|
|
51
|
+
* `requestId`. Default `false`: it costs a header read and 8 random bytes, and
|
|
52
|
+
* `requestId` already spans two dunx services. `@dunx/http/client` sends the
|
|
53
|
+
* adopted trace upstream.
|
|
112
54
|
*/
|
|
113
55
|
readonly trace?: boolean;
|
|
114
56
|
}
|
|
115
57
|
/**
|
|
116
58
|
* One structured entry per request, carrying the request and its response.
|
|
59
|
+
* Installed by `HttpFactory.create` unless `requestLogging: false`, and injecting
|
|
60
|
+
* only core contracts, so it works with no logging module imported.
|
|
117
61
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* `@arkv/logger` automatically once `@dunx/infra/logger` is.
|
|
122
|
-
*
|
|
123
|
-
* **One entry, not two.** A framework whose middleware cannot see the response
|
|
124
|
-
* needs a middleware for the inbound half and an
|
|
125
|
-
* interceptor for the outbound one, because they are different classes and the
|
|
126
|
-
* interceptor cannot see what the middleware saw. Here they are the same
|
|
127
|
-
* closure, so there is no pair to correlate by `requestId` to find out how a
|
|
128
|
-
* call ended. A 4xx is the same line at `warn`, a 5xx at `error`.
|
|
129
|
-
*
|
|
130
|
-
* Everything the handler logs in between carries `requestId`, `method`, `event`
|
|
131
|
-
* and `context` without being passed anything, because the whole call runs
|
|
132
|
-
* inside `runWithContext` - unless `correlate: false`, which drops the scope and
|
|
133
|
-
* with it that guarantee, but not the fields on this middleware's own entry.
|
|
62
|
+
* One entry rather than a middleware and an interceptor to correlate: middleware
|
|
63
|
+
* wraps `next()`, so both halves are the same closure. A 4xx logs at `warn`, a
|
|
64
|
+
* 5xx at `error`.
|
|
134
65
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* with `.then` rather than awaited - the same rule `input.ts` follows, for the
|
|
138
|
-
* same measured reason. An `async` scope callback alone cost 0.44 µs/request
|
|
139
|
-
* against a synchronous one on raw `Bun.serve`.
|
|
66
|
+
* Nothing here is `async`. The two steps that can wait are off by default and
|
|
67
|
+
* adopted with `.then`; an `async` scope callback alone cost 0.44 us/request.
|
|
140
68
|
*/
|
|
141
69
|
export declare class RequestLoggingMiddleware implements Middleware {
|
|
142
70
|
#private;
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -39,19 +39,12 @@ export declare const assertNoGatewayCollisions: (discovered: readonly Discovered
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const withUpgradeRoutes: (routes: BunRoutes, gateways: ReadonlyMap<string, UpgradeHandler>) => ServeRoutes;
|
|
41
41
|
/**
|
|
42
|
-
* Bun answers an unmatched path itself, so nothing in the middleware chain
|
|
43
|
-
*
|
|
42
|
+
* Bun answers an unmatched path itself, so nothing in the middleware chain sees
|
|
43
|
+
* it and a 404 is invisible to request logging. This is the only `fetch` handler
|
|
44
|
+
* dunx installs and it is not a router: it runs once Bun has matched nothing.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* matched. It puts the global middleware in front of a 404 in the framework's
|
|
48
|
-
* own error shape.
|
|
49
|
-
*
|
|
50
|
-
* **The miss is a `throw`, not a returned `Response`.** `miss` raises
|
|
51
|
-
* `HttpError(404)` and `compose` propagates it, so a middleware written as
|
|
52
|
-
* `const response = await next(); if (response.status === 404) ...` never reaches
|
|
53
|
-
* its own second line on an unmatched path - the rewrite it was written for is the
|
|
54
|
-
* one case it cannot see. A middleware that means to act on a miss has to catch:
|
|
46
|
+
* The miss is a throw, not a returned `Response`, so a middleware reading
|
|
47
|
+
* `(await next()).status` never sees one and has to catch:
|
|
55
48
|
*
|
|
56
49
|
* ```ts
|
|
57
50
|
* try {
|
|
@@ -62,13 +55,8 @@ export declare const withUpgradeRoutes: (routes: BunRoutes, gateways: ReadonlyMa
|
|
|
62
55
|
* }
|
|
63
56
|
* ```
|
|
64
57
|
*
|
|
65
|
-
* `ctx.get(UNMATCHED)` is the
|
|
66
|
-
*
|
|
67
|
-
* handler answered 404 for a record that does not exist" **before** calling
|
|
68
|
-
* `next()` at all. Only the second of those is a `Response` to inspect.
|
|
69
|
-
*
|
|
70
|
-
* Composed per request rather than at boot, because the context names the path
|
|
71
|
-
* that missed. That allocation is on the 404 path only.
|
|
58
|
+
* `ctx.get(UNMATCHED)` is the cheaper half: set here and by no real route, so a
|
|
59
|
+
* middleware can tell a miss from a handler's own 404 before calling `next()`.
|
|
72
60
|
*/
|
|
73
61
|
export declare const buildFallback: (middleware?: readonly Middleware[], onError?: ErrorMapper, cors?: CorsOptions, notFound?: 'guarded' | 'public') => RouteHandler;
|
|
74
62
|
export declare const buildRoutes: (discovered: readonly DiscoveredRoute[], middleware?: readonly Middleware[], onError?: ErrorMapper, cors?: CorsOptions, resolve?: GuardResolver) => BunRoutes;
|
|
@@ -27,17 +27,13 @@ export interface Trace {
|
|
|
27
27
|
export declare class TraceContext {
|
|
28
28
|
#private;
|
|
29
29
|
/**
|
|
30
|
-
* The inbound `traceparent`, or a fresh trace.
|
|
30
|
+
* The inbound `traceparent`, or a fresh trace. A malformed header is discarded
|
|
31
|
+
* rather than repaired, as the standard requires. Version `ff` is invalid; a
|
|
32
|
+
* higher version keeps its first four fields, so a future format still
|
|
33
|
+
* propagates.
|
|
31
34
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* unknown, not that this request has none. Version `ff` is invalid, and a
|
|
35
|
-
* higher version keeps its first four fields and drops the rest, so a future
|
|
36
|
-
* format still propagates through this service instead of being dropped.
|
|
37
|
-
*
|
|
38
|
-
* When nothing arrives, `traceId` is the request id with its hyphens removed -
|
|
39
|
-
* a UUID is 16 bytes, which is exactly a trace id, and reusing it means one
|
|
40
|
-
* identifier in two spellings rather than a second `crypto` call per request.
|
|
35
|
+
* With nothing inbound, `traceId` is the request id minus its hyphens - a UUID
|
|
36
|
+
* is 16 bytes, exactly a trace id, so there is no second `crypto` call.
|
|
41
37
|
*/
|
|
42
38
|
static adopt(req: Request, requestId: string): Trace;
|
|
43
39
|
/** The trace adopted for this request, if one was. */
|
package/dist/static/files.d.ts
CHANGED
|
@@ -3,20 +3,12 @@ import type { Middleware, Next } from '../server/middleware.js';
|
|
|
3
3
|
import type { RouteContext } from '../server/context.js';
|
|
4
4
|
import { StaticOptions } from './options.js';
|
|
5
5
|
/**
|
|
6
|
-
* Static files, on `Bun.file`.
|
|
6
|
+
* Static files, on `Bun.file`. A `Bun.file` handed to a `Response` already
|
|
7
|
+
* streams, sets `content-type`, answers a `Range` request and uses `sendfile(2)`,
|
|
8
|
+
* so this file is a path check and a cache policy.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* None of that is needed here: `Bun.file(path)` handed to a `Response` already
|
|
11
|
-
* streams, already sets `content-type` from the extension, already answers a
|
|
12
|
-
* `Range` request, and does the whole thing with `sendfile(2)` rather than reading
|
|
13
|
-
* into JavaScript. So this file is a **path check and a cache policy**, and that is
|
|
14
|
-
* the entire justification for it existing.
|
|
15
|
-
*
|
|
16
|
-
* A middleware rather than routes, for the same reason the dashboard is one: the
|
|
17
|
-
* file set is whatever is on disk at request time, and turning it into a
|
|
18
|
-
* `Bun.serve` route table would mean walking a directory at boot and being wrong
|
|
19
|
-
* the moment anything changed.
|
|
10
|
+
* A middleware rather than routes: the file set is whatever is on disk at request
|
|
11
|
+
* time, and a route table would be walked at boot and wrong thereafter.
|
|
20
12
|
*/
|
|
21
13
|
export declare class StaticFiles implements Middleware {
|
|
22
14
|
#private;
|
package/dist/static/module.d.ts
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
|
|
2
2
|
import { type StaticOptionsInit } from './options.js';
|
|
3
3
|
/**
|
|
4
|
-
* Serves a directory
|
|
5
|
-
* dashboard, **it does not register itself**. The app does:
|
|
4
|
+
* Serves a directory. Like the dashboard, it does not register itself:
|
|
6
5
|
*
|
|
7
6
|
* ```ts
|
|
8
7
|
* const app = await HttpFactory.create(AppModule);
|
|
9
8
|
* app.use(StaticFiles);
|
|
10
9
|
* ```
|
|
11
10
|
*
|
|
12
|
-
* Position in the chain is
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the app's own routes and its 404 behave exactly as before.
|
|
11
|
+
* Position in the chain is left to the app - assets usually want to be outside an
|
|
12
|
+
* auth guard and inside request logging. Anything outside the mount falls through
|
|
13
|
+
* untouched.
|
|
16
14
|
*
|
|
17
|
-
* There is no `index.html` fallback and no SPA rewrite
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* matters. An unmatched path is a **thrown** `HttpError(404)`, not a returned
|
|
22
|
-
* `Response` - see `buildFallback` - so reading `(await next()).status` never sees a
|
|
23
|
-
* miss, and `ctx.get(UNMATCHED)` is what does:
|
|
15
|
+
* There is no `index.html` fallback and no SPA rewrite, which would mean this
|
|
16
|
+
* middleware deciding what a 404 means for paths it does not own. An app that
|
|
17
|
+
* wants one writes a middleware outside this one, reading `ctx.get(UNMATCHED)`
|
|
18
|
+
* rather than a returned status, since a miss is thrown:
|
|
24
19
|
*
|
|
25
20
|
* ```ts
|
|
26
21
|
* export class SpaFallback implements Middleware {
|
|
@@ -45,12 +40,8 @@ import { type StaticOptionsInit } from './options.js';
|
|
|
45
40
|
* }
|
|
46
41
|
* ```
|
|
47
42
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* refuses it and the status is a 401 rather than a 404; an app serving a SPA wants
|
|
51
|
-
* `notFound: 'public'`. And the fallback answers **before** any middleware listed
|
|
52
|
-
* after the guard, so the rewrite has to sit ahead of the guard to see the miss at
|
|
53
|
-
* all.
|
|
43
|
+
* `notFound: 'guarded'`, the default, reports a miss with no route metadata, so a
|
|
44
|
+
* session guard refuses it with a 401; a SPA wants `notFound: 'public'`.
|
|
54
45
|
*/
|
|
55
46
|
export declare class StaticModule {
|
|
56
47
|
static forRoot(init: StaticOptionsInit): DynamicModule;
|
package/dist/throttle/guard.d.ts
CHANGED
|
@@ -8,17 +8,14 @@ import { ThrottleStore } from './store.js';
|
|
|
8
8
|
/**
|
|
9
9
|
* A fixed-window rate limit, one key per subject and handler.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* dead service.
|
|
11
|
+
* Fails open: an unreachable store allows the request and warns once per process,
|
|
12
|
+
* since refusing everything because the counter is down turns a degraded
|
|
13
|
+
* dependency into a dead service.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
* user id
|
|
18
|
-
* knows which - which is what `ThrottleOptions.subject` reads.
|
|
15
|
+
* List it after any session guard - only the guard ahead knows whether to limit by
|
|
16
|
+
* user id or by address, which is what `ThrottleOptions.subject` reads.
|
|
19
17
|
*
|
|
20
|
-
* The 429 is thrown,
|
|
21
|
-
* comes out in the app's error shape like every other status.
|
|
18
|
+
* The 429 is thrown, so it comes out in the app's own error shape.
|
|
22
19
|
*/
|
|
23
20
|
export declare class ThrottleGuard implements Middleware {
|
|
24
21
|
#private;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
|
|
2
2
|
import { type ThrottleOptionsInit } from './options.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The decorator, the guard, the counter and its options.
|
|
5
5
|
*
|
|
6
|
-
* `global: true
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* never names.
|
|
6
|
+
* `global: true`: the guard is listed in `HttpOptions.middleware`, the app's own
|
|
7
|
+
* list, so a non-global module would make every consumer import this one to reach
|
|
8
|
+
* a guard it never names.
|
|
10
9
|
*
|
|
11
10
|
* ```ts
|
|
12
11
|
* ThrottleModule.forRootAsync({
|
|
@@ -22,9 +21,8 @@ import { type ThrottleOptionsInit } from './options.js';
|
|
|
22
21
|
* HttpFactory.create(AppModule, { middleware: [SessionGuard, ThrottleGuard] });
|
|
23
22
|
* ```
|
|
24
23
|
*
|
|
25
|
-
* Position in the chain is the app's
|
|
26
|
-
*
|
|
27
|
-
* as an address.
|
|
24
|
+
* Position in the chain is the app's: ahead of a session guard, the limit counts
|
|
25
|
+
* every caller as an address.
|
|
28
26
|
*/
|
|
29
27
|
export declare class ThrottleModule {
|
|
30
28
|
static forRoot(init: ThrottleOptionsInit): DynamicModule;
|
package/dist/throttle/store.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The counter behind the guard.
|
|
2
|
+
* The counter behind the guard. An `abstract class` rather than an interface,
|
|
3
|
+
* since an interface at an injection site is a boot error.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* **A fixed window, not a sliding one.** `hit` increments and returns the count for
|
|
9
|
-
* the window the key is already in; the window starts at the first hit and ends
|
|
10
|
-
* when the key expires. A sliding window needs a sorted set per subject and a
|
|
11
|
-
* range trim per request, which is a different cost for an accuracy a rate limit
|
|
12
|
-
* does not need.
|
|
5
|
+
* A fixed window: `hit` returns the count for the window the key is already in,
|
|
6
|
+
* which starts at the first hit and ends when the key expires. A sliding window
|
|
7
|
+
* needs a sorted set per subject and a trim per request.
|
|
13
8
|
*/
|
|
14
9
|
export declare abstract class ThrottleStore {
|
|
15
10
|
constructor();
|
|
@@ -51,17 +46,13 @@ export declare class RedisThrottleStore extends ThrottleStore {
|
|
|
51
46
|
ttl(key: string): Promise<number | undefined>;
|
|
52
47
|
}
|
|
53
48
|
/**
|
|
54
|
-
* The single-process counter
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* It is per process, which is the whole caveat: two replicas each allow the full
|
|
58
|
-
* budget. `RedisThrottleStore` is the answer for more than one.
|
|
49
|
+
* The single-process counter and the default, so an app with no Redis still limits
|
|
50
|
+
* something. Per process is the caveat: two replicas each allow the full budget,
|
|
51
|
+
* and `RedisThrottleStore` is the answer for more than one.
|
|
59
52
|
*
|
|
60
|
-
* The map is bounded.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* expired clears it, which resets a window early rather than holding memory a
|
|
64
|
-
* server does not have.
|
|
53
|
+
* The map is bounded. Expired entries are dropped on touch and swept past
|
|
54
|
+
* `maxKeys`; reaching the cap with nothing expired clears it, resetting a window
|
|
55
|
+
* early rather than holding memory.
|
|
65
56
|
*/
|
|
66
57
|
export declare class MemoryThrottleStore extends ThrottleStore {
|
|
67
58
|
#private;
|
package/dist/ws/middleware.d.ts
CHANGED
|
@@ -43,35 +43,26 @@ export interface SocketFrame {
|
|
|
43
43
|
*/
|
|
44
44
|
export type SocketNext = () => unknown;
|
|
45
45
|
/**
|
|
46
|
-
* The single extension point
|
|
47
|
-
*
|
|
46
|
+
* The socket side's single extension point, shaped like {@link Middleware}: one
|
|
47
|
+
* method wrapping `next()`. It sees every dispatched handler, and open and close
|
|
48
|
+
* arrive even for a gateway declaring neither.
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* declares no `@OnOpen`/`@OnClose`, so a connection is never invisible to it.
|
|
50
|
+
* A throwing handler passes through here. Rethrow to leave the outcome to
|
|
51
|
+
* `SocketOptions.onError`, or return a value to answer the frame.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* to `SocketOptions.onError`; return a value instead to answer the frame.
|
|
56
|
-
*
|
|
57
|
-
* Three things it cannot see, because they never reach the dispatcher: a
|
|
58
|
-
* `socket.send` a handler makes itself, a `PubSub` broadcast, and the upgrade -
|
|
59
|
-
* which is an HTTP request answered by the gateway's own route.
|
|
53
|
+
* It cannot see a `socket.send` a handler makes itself, a `PubSub` broadcast, or
|
|
54
|
+
* the upgrade, which is an HTTP request.
|
|
60
55
|
*/
|
|
61
56
|
export interface SocketMiddleware {
|
|
62
57
|
/**
|
|
63
|
-
* That a failure passing through here is reported somewhere. Default
|
|
64
|
-
* **`false`**.
|
|
58
|
+
* That a failure passing through here is reported somewhere. Default `false`.
|
|
65
59
|
*
|
|
66
60
|
* `SocketOptions.onError`'s `console.error` fallback is not installed while any
|
|
67
|
-
* socket middleware exists,
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* whole server, and nothing about the wiring says so.
|
|
61
|
+
* socket middleware exists, since a middleware would report the same failure
|
|
62
|
+
* twice. Only the middleware knows whether it does, and one that ignores a throw
|
|
63
|
+
* would silently turn error reporting off for the whole server.
|
|
71
64
|
*
|
|
72
|
-
*
|
|
73
|
-
* `websocket.onError` beside it is what `HttpFactory.create` warns about at
|
|
74
|
-
* boot.
|
|
65
|
+
* Unset with no `websocket.onError` beside it is what `create` warns about.
|
|
75
66
|
*/
|
|
76
67
|
readonly reportsErrors?: boolean;
|
|
77
68
|
handle(frame: SocketFrame, ctx: SocketContext, next: SocketNext): unknown;
|