@znight/http 0.5.0 → 0.6.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 +15 -0
- package/dist/adapter-6qC95ViO.d.mts +2163 -0
- package/dist/compiler.d.mts +1 -1
- package/dist/compiler.mjs +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.mjs +4 -1
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.mjs +4 -1
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.mjs +38 -3
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +3 -1117
- package/dist/index.mjs +263 -269
- package/dist/index.mjs.map +1 -1
- package/dist/{node-DnYv_AMI.mjs → node-npGnu7IW.mjs} +5 -2
- package/dist/node-npGnu7IW.mjs.map +1 -0
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{node-io-BkdRSQeg.mjs → upgrade-io-dTMDDmBi.mjs} +625 -27
- package/dist/upgrade-io-dTMDDmBi.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapter.ts +7 -0
- package/src/adapters/express.ts +4 -0
- package/src/adapters/fastify.ts +4 -0
- package/src/adapters/hono.ts +48 -2
- package/src/adapters/node-io.ts +8 -0
- package/src/adapters/node.ts +4 -0
- package/src/adapters/upgrade-io.ts +255 -0
- package/src/index.ts +14 -0
- package/src/pipeline.ts +108 -2
- package/src/plugin-routes.ts +6 -0
- package/src/plugin.ts +197 -16
- package/src/upgrade.ts +341 -0
- package/dist/adapter-DmhQGqAR.d.mts +0 -895
- package/dist/node-DnYv_AMI.mjs.map +0 -1
- package/dist/node-io-BkdRSQeg.mjs.map +0 -1
|
@@ -0,0 +1,2163 @@
|
|
|
1
|
+
import { t as RoutePath } from "./path-JI87C4k8.mjs";
|
|
2
|
+
import { ClassMarker, Container, MethodMarker, ParameterMarker, Plugin, PluginContext, PluginOptions, ProviderKey, Scope, Token } from "@znight/core";
|
|
3
|
+
/**
|
|
4
|
+
* Cookies (RFC 0006): parsing of the `cookie` request header, canonical value encoding, strict
|
|
5
|
+
* `Set-Cookie` serialisation (RFC 6265bis: attribute validation, name prefixes, size limits),
|
|
6
|
+
* `Set-Cookie` parsing (tests) and the response cookie jar.
|
|
7
|
+
*
|
|
8
|
+
* This module is import-free (pure functions): safe to import from anywhere, the compiler excepted
|
|
9
|
+
* (it has its own token grammar).
|
|
10
|
+
*/
|
|
11
|
+
/** Options of a cookie set on a response (`ctx.setCookie()`, `HttpResponse.withCookie()`). */
|
|
12
|
+
interface CookieOptions {
|
|
13
|
+
/** Lifetime in seconds (integer). `0` or less expires the cookie now. Default: session cookie. */
|
|
14
|
+
readonly maxAge?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Absolute expiry (sent as an IMF-fixdate), in the years 1601 to 9999 (browsers ignore other
|
|
17
|
+
* years). Browsers cap lifetimes at 400 days: prefer `maxAge` for long-lived cookies.
|
|
18
|
+
*/
|
|
19
|
+
readonly expires?: Date;
|
|
20
|
+
/**
|
|
21
|
+
* Domain (sub-domains included). Default: none, a host-only cookie. `null`: host-only even when
|
|
22
|
+
* `cookies.defaults` sets a domain. `__Host-` cookies never take the default domain.
|
|
23
|
+
*/
|
|
24
|
+
readonly domain?: string | null;
|
|
25
|
+
/** Default: `'/'`. */
|
|
26
|
+
readonly path?: string;
|
|
27
|
+
/** Default: `true` (browsers only send it over HTTPS, `http://localhost` excepted). */
|
|
28
|
+
readonly secure?: boolean;
|
|
29
|
+
/** Default: `true` (not readable from JavaScript). */
|
|
30
|
+
readonly httpOnly?: boolean;
|
|
31
|
+
/** Default: `'lax'`. `'none'` requires `secure`. */
|
|
32
|
+
readonly sameSite?: "strict" | "lax" | "none";
|
|
33
|
+
/** CHIPS partitioned cookie. Requires `secure`. Default: `false`. */
|
|
34
|
+
readonly partitioned?: boolean;
|
|
35
|
+
/** Chromium `Priority`. Default: none. */
|
|
36
|
+
readonly priority?: "low" | "medium" | "high";
|
|
37
|
+
/** Signs the value with the first secret of the plugin option `cookies.secrets`. Default: `false`. */
|
|
38
|
+
readonly signed?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Plugin-wide defaults (`cookies.defaults`). Lifetime and signing are per cookie. */
|
|
41
|
+
type CookieDefaults = Omit<CookieOptions, "maxAge" | "expires" | "signed">;
|
|
42
|
+
/** What identifies a cookie to clear (and keeps prefixed names valid). */
|
|
43
|
+
type ClearCookieOptions = Pick<CookieOptions, "domain" | "path" | "secure" | "httpOnly" | "sameSite" | "partitioned">;
|
|
44
|
+
/** A cookie to send, as given (defaults and signing are applied when the response is serialised). */
|
|
45
|
+
interface ResponseCookie {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** `undefined`: a clearing cookie. */
|
|
48
|
+
readonly value: string | undefined;
|
|
49
|
+
readonly options: CookieOptions;
|
|
50
|
+
}
|
|
51
|
+
/** A parsed `Set-Cookie` line (see {@link parseSetCookie}). */
|
|
52
|
+
interface SetCookieInfo {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
/** Decoded value (a signed cookie: `"<value>.<signature>"`, decoded). */
|
|
55
|
+
readonly value: string;
|
|
56
|
+
/** The header line. */
|
|
57
|
+
readonly raw: string;
|
|
58
|
+
readonly expires?: Date;
|
|
59
|
+
readonly maxAge?: number;
|
|
60
|
+
readonly domain?: string;
|
|
61
|
+
readonly path?: string;
|
|
62
|
+
readonly secure: boolean;
|
|
63
|
+
readonly httpOnly: boolean;
|
|
64
|
+
readonly sameSite?: "strict" | "lax" | "none";
|
|
65
|
+
readonly partitioned: boolean;
|
|
66
|
+
readonly priority?: "low" | "medium" | "high";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parses a `cookie` request header into a prototype-less, frozen record (`__proto__` is just a
|
|
70
|
+
* name). Pairs are split on `;` only (never `,`) and trimmed; a pair without `=` or with an empty
|
|
71
|
+
* name is ignored; names are kept as sent (case-sensitive); a value wrapped in double quotes is
|
|
72
|
+
* unquoted, and percent-decoded when it contains `%` (a malformed sequence keeps the raw value).
|
|
73
|
+
* When a name repeats, the first occurrence wins (browsers send the most specific path first).
|
|
74
|
+
* Linear time, never throws.
|
|
75
|
+
*/
|
|
76
|
+
declare function parseCookieHeader(header: string | undefined): Readonly<Record<string, string>>;
|
|
77
|
+
/**
|
|
78
|
+
* The `Set-Cookie` line of a cookie with the secure defaults
|
|
79
|
+
* (`Path=/; Secure; HttpOnly; SameSite=Lax`, overridable per cookie), in the order
|
|
80
|
+
* `name=value; Max-Age; Expires; Domain; Path; Secure; HttpOnly; SameSite; Partitioned; Priority`.
|
|
81
|
+
* The value is percent-encoded (see {@link encodeCookieValue}). Signing (`signed: true`) needs the
|
|
82
|
+
* secrets of the plugin: use `ctx.setCookie()` or `HttpPlugin.signCookie()`.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* serializeCookie('theme', 'dark', { maxAge: 86_400, httpOnly: false })
|
|
86
|
+
* // theme=dark; Max-Age=86400; Path=/; Secure; SameSite=Lax
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @throws TypeError for an invalid name, value or attribute, a `SameSite=None` or partitioned cookie
|
|
90
|
+
* without `Secure`, a broken `__Secure-` / `__Host-` / `__Http-` / `__Host-Http-` prefix rule.
|
|
91
|
+
* @throws RangeError when `name=value` exceeds 4096 bytes, or `Domain` / `Path` 1024 bytes.
|
|
92
|
+
*/
|
|
93
|
+
declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
|
|
94
|
+
/**
|
|
95
|
+
* The `Set-Cookie` line clearing a cookie: `name=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
|
|
96
|
+
* plus `Domain`, `Path`, `Secure`, `HttpOnly`, `SameSite` and `Partitioned` (secure defaults merged
|
|
97
|
+
* with `options`), so that a prefixed cookie is cleared with valid attributes.
|
|
98
|
+
*/
|
|
99
|
+
declare function serializeClearCookie(name: string, options?: ClearCookieOptions): string;
|
|
100
|
+
/**
|
|
101
|
+
* Parses a `Set-Cookie` line (attribute names are case-insensitive; the value is decoded like a
|
|
102
|
+
* request cookie). Used by `inject()` to fill `response.cookies`.
|
|
103
|
+
*
|
|
104
|
+
* @throws TypeError when the line has no `name=value` pair.
|
|
105
|
+
*/
|
|
106
|
+
declare function parseSetCookie(line: string): SetCookieInfo;
|
|
107
|
+
/**
|
|
108
|
+
* A copy of the [Standard Schema v1](https://standardschema.dev) interface, flattened (no namespace,
|
|
109
|
+
* so the file stays erasable). Every compliant library (Zod, Valibot, ArkType, Effect Schema...)
|
|
110
|
+
* produces values assignable to {@link StandardSchemaV1}: no adapter and no dependency are needed.
|
|
111
|
+
*/
|
|
112
|
+
/** A schema implementing the Standard Schema v1 interface. */
|
|
113
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
114
|
+
/** The Standard Schema properties. */
|
|
115
|
+
readonly "~standard": StandardSchemaV1Props<Input, Output>;
|
|
116
|
+
}
|
|
117
|
+
/** The Standard Schema properties. */
|
|
118
|
+
interface StandardSchemaV1Props<Input = unknown, Output = Input> {
|
|
119
|
+
/** The version number of the standard. */
|
|
120
|
+
readonly version: 1;
|
|
121
|
+
/** The vendor name of the schema library. */
|
|
122
|
+
readonly vendor: string;
|
|
123
|
+
/** Validates an unknown input value (synchronously or not). */
|
|
124
|
+
readonly validate: (value: unknown, options?: StandardSchemaV1Options | undefined) => StandardSchemaV1Result<Output> | Promise<StandardSchemaV1Result<Output>>;
|
|
125
|
+
/** Inferred types associated with the schema. */
|
|
126
|
+
readonly types?: StandardSchemaV1Types<Input, Output> | undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Options of {@link StandardSchemaV1Props.validate}. */
|
|
129
|
+
interface StandardSchemaV1Options {
|
|
130
|
+
/** Vendor-specific parameters. */
|
|
131
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
132
|
+
}
|
|
133
|
+
/** Result of {@link StandardSchemaV1Props.validate}. */
|
|
134
|
+
type StandardSchemaV1Result<Output> = StandardSchemaV1SuccessResult<Output> | StandardSchemaV1FailureResult;
|
|
135
|
+
/** Successful validation: the (possibly transformed) output value. */
|
|
136
|
+
interface StandardSchemaV1SuccessResult<Output> {
|
|
137
|
+
readonly value: Output;
|
|
138
|
+
readonly issues?: undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Failed validation. */
|
|
141
|
+
interface StandardSchemaV1FailureResult {
|
|
142
|
+
readonly issues: readonly StandardSchemaV1Issue[];
|
|
143
|
+
}
|
|
144
|
+
/** One validation issue. */
|
|
145
|
+
interface StandardSchemaV1Issue {
|
|
146
|
+
readonly message: string;
|
|
147
|
+
readonly path?: readonly (PropertyKey | StandardSchemaV1PathSegment)[] | undefined;
|
|
148
|
+
}
|
|
149
|
+
/** A path segment of an issue. */
|
|
150
|
+
interface StandardSchemaV1PathSegment {
|
|
151
|
+
readonly key: PropertyKey;
|
|
152
|
+
}
|
|
153
|
+
/** Input and output types of a schema. */
|
|
154
|
+
interface StandardSchemaV1Types<Input = unknown, Output = Input> {
|
|
155
|
+
readonly input: Input;
|
|
156
|
+
readonly output: Output;
|
|
157
|
+
}
|
|
158
|
+
/** The input type of a schema. */
|
|
159
|
+
type InferSchemaInput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["input"];
|
|
160
|
+
/** The output type of a schema (what handlers receive). */
|
|
161
|
+
type InferSchemaOutput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["output"];
|
|
162
|
+
/** Whether `value` looks like a Standard Schema v1 (has a `~standard` object with a `validate` function). */
|
|
163
|
+
declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
164
|
+
/** Response header values: a single value or several (`set-cookie`). */
|
|
165
|
+
type HeaderValue = string | readonly string[];
|
|
166
|
+
/** Options of {@link HttpException}. */
|
|
167
|
+
interface HttpExceptionOptions extends ErrorOptions {
|
|
168
|
+
/** Headers added to the error response (`www-authenticate`, `retry-after`, `allow`...). */
|
|
169
|
+
readonly headers?: Readonly<Record<string, HeaderValue>>;
|
|
170
|
+
}
|
|
171
|
+
/** JSON body of an error response. */
|
|
172
|
+
interface HttpErrorBody {
|
|
173
|
+
readonly statusCode: number;
|
|
174
|
+
/** Reason phrase of the status (`Not Found`). */
|
|
175
|
+
readonly error: string;
|
|
176
|
+
readonly message: string;
|
|
177
|
+
/** Structured details (validation issues...), omitted when absent. */
|
|
178
|
+
readonly details?: unknown;
|
|
179
|
+
}
|
|
180
|
+
/** One entry of the `details` of a validation error. */
|
|
181
|
+
interface ValidationIssueDetail {
|
|
182
|
+
/** Dotted path of the invalid value inside the validated input (`user.email`, `items.0`), `''` for the root. */
|
|
183
|
+
readonly path: string;
|
|
184
|
+
readonly message: string;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* An error carrying an HTTP status. Thrown from handlers, guards, interceptors or services, it is
|
|
188
|
+
* rendered as `{ statusCode, error, message, details? }` with its status and headers.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* throw new HttpException(409, 'Email already used', { field: 'email' })
|
|
192
|
+
* throw new NotFoundException(`User ${id} not found`)
|
|
193
|
+
* throw new UnauthorizedException('Token expired', undefined, { headers: { 'www-authenticate': 'Bearer' } })
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* Invalid statuses (not an integer in 100–599) become `500`.
|
|
197
|
+
*/
|
|
198
|
+
declare class HttpException extends Error {
|
|
199
|
+
/** HTTP status of the response. */
|
|
200
|
+
readonly status: number;
|
|
201
|
+
/** Structured details rendered in the body (omitted when `undefined`). */
|
|
202
|
+
readonly details: unknown;
|
|
203
|
+
/** Headers added to the response. */
|
|
204
|
+
readonly headers: Readonly<Record<string, HeaderValue>>;
|
|
205
|
+
constructor(status: number, message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
206
|
+
/** The `statusCode` field of the body (alias of {@link status}). */
|
|
207
|
+
get statusCode(): number;
|
|
208
|
+
/** Body of the error response. Override it in a subclass to change the shape. */
|
|
209
|
+
toBody(): HttpErrorBody;
|
|
210
|
+
}
|
|
211
|
+
/** Whether `value` is an {@link HttpException} (also across duplicated package copies). */
|
|
212
|
+
declare function isHttpException(value: unknown): value is HttpException;
|
|
213
|
+
/** 400 Bad Request. */
|
|
214
|
+
declare class BadRequestException extends HttpException {
|
|
215
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 400 Bad Request raised when an input fails its schema (`@Body(schema)`, `@Query(schema)`...).
|
|
219
|
+
* `details` lists the issues as `{ path, message }`; the raw Standard Schema issues are kept in
|
|
220
|
+
* {@link issues} for exception mappers.
|
|
221
|
+
*/
|
|
222
|
+
declare class ValidationException extends BadRequestException {
|
|
223
|
+
/** What was validated: `body`, `query`, `query "page"`, `param "id"`... */
|
|
224
|
+
readonly location: string;
|
|
225
|
+
readonly issues: readonly StandardSchemaV1Issue[];
|
|
226
|
+
constructor(location: string, issues: readonly StandardSchemaV1Issue[], options?: HttpExceptionOptions);
|
|
227
|
+
}
|
|
228
|
+
/** Converts a Standard Schema issue to `{ path, message }` (`path` dotted, `''` for the root). */
|
|
229
|
+
declare function toIssueDetail(issue: StandardSchemaV1Issue): ValidationIssueDetail;
|
|
230
|
+
/** 401 Unauthorized. */
|
|
231
|
+
declare class UnauthorizedException extends HttpException {
|
|
232
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
233
|
+
}
|
|
234
|
+
/** 403 Forbidden (also the response of a guard returning `false`). */
|
|
235
|
+
declare class ForbiddenException extends HttpException {
|
|
236
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
237
|
+
}
|
|
238
|
+
/** 404 Not Found. */
|
|
239
|
+
declare class NotFoundException extends HttpException {
|
|
240
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
241
|
+
}
|
|
242
|
+
/** 405 Method Not Allowed (pass the `allow` header in `options.headers`). */
|
|
243
|
+
declare class MethodNotAllowedException extends HttpException {
|
|
244
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
245
|
+
}
|
|
246
|
+
/** 409 Conflict. */
|
|
247
|
+
declare class ConflictException extends HttpException {
|
|
248
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
249
|
+
}
|
|
250
|
+
/** 410 Gone. */
|
|
251
|
+
declare class GoneException extends HttpException {
|
|
252
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
253
|
+
}
|
|
254
|
+
/** 413 Payload Too Large (request body above `bodyLimit`). */
|
|
255
|
+
declare class PayloadTooLargeException extends HttpException {
|
|
256
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
257
|
+
}
|
|
258
|
+
/** 415 Unsupported Media Type. */
|
|
259
|
+
declare class UnsupportedMediaTypeException extends HttpException {
|
|
260
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
261
|
+
}
|
|
262
|
+
/** 422 Unprocessable Entity. */
|
|
263
|
+
declare class UnprocessableEntityException extends HttpException {
|
|
264
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
265
|
+
}
|
|
266
|
+
/** 429 Too Many Requests (pass `retry-after` in `options.headers`). */
|
|
267
|
+
declare class TooManyRequestsException extends HttpException {
|
|
268
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
269
|
+
}
|
|
270
|
+
/** 500 Internal Server Error (its message is always sent, unlike unexpected errors). */
|
|
271
|
+
declare class InternalServerErrorException extends HttpException {
|
|
272
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
273
|
+
}
|
|
274
|
+
/** 501 Not Implemented. */
|
|
275
|
+
declare class NotImplementedException extends HttpException {
|
|
276
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
277
|
+
}
|
|
278
|
+
/** 502 Bad Gateway. */
|
|
279
|
+
declare class BadGatewayException extends HttpException {
|
|
280
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
281
|
+
}
|
|
282
|
+
/** 503 Service Unavailable. */
|
|
283
|
+
declare class ServiceUnavailableException extends HttpException {
|
|
284
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
285
|
+
}
|
|
286
|
+
/** 504 Gateway Timeout. */
|
|
287
|
+
declare class GatewayTimeoutException extends HttpException {
|
|
288
|
+
constructor(message?: string, details?: unknown, options?: HttpExceptionOptions);
|
|
289
|
+
}
|
|
290
|
+
/** Parsed query string (or `application/x-www-form-urlencoded` body): repeated keys become arrays. */
|
|
291
|
+
type QueryRecord = Record<string, string | string[]>;
|
|
292
|
+
/**
|
|
293
|
+
* Parses a query string (with or without the leading `?`) with `URLSearchParams` semantics (`+` is
|
|
294
|
+
* a space, malformed `%` sequences are kept as is, invalid UTF-8 becomes U+FFFD). A key present
|
|
295
|
+
* once maps to a string, a repeated key to an array of strings, in order. No nesting (`a[b]=1` is
|
|
296
|
+
* the key `a[b]`). The returned object has no prototype.
|
|
297
|
+
*
|
|
298
|
+
* `parseQuery('?tag=a&tag=b&page=2')` → `{ tag: ['a', 'b'], page: '2' }`
|
|
299
|
+
*/
|
|
300
|
+
declare function parseQuery(query: string): QueryRecord;
|
|
301
|
+
/**
|
|
302
|
+
* Byte sizes (`1048576`, `'512kb'`, `'10 MiB'`, `'1.5gb'`) used by body and upload limits. This
|
|
303
|
+
* module is **pure** (no imports, no I/O): the compiler extension evaluates `@BodyLimit('10mb')` and
|
|
304
|
+
* `maxSize` options with it at build time.
|
|
305
|
+
*
|
|
306
|
+
* Units are case-insensitive and binary, like the `bytes` package used by Express:
|
|
307
|
+
* `b`, `kb`/`kib`/`k` = 1024, `mb`/`mib`/`m` = 1024², `gb`/`gib`/`g` = 1024³, `tb`/`tib`/`t` = 1024⁴.
|
|
308
|
+
*/
|
|
309
|
+
/**
|
|
310
|
+
* A size in bytes: a non-negative integer (`Infinity` disables the limit) or a string with a unit
|
|
311
|
+
* (`'512kb'`, `'10mb'`, `'1.5 GiB'`; `kb` = 1024 bytes).
|
|
312
|
+
*/
|
|
313
|
+
type ByteSize = number | string;
|
|
314
|
+
/**
|
|
315
|
+
* Parses a {@link ByteSize}: `1024` → `1024`, `'10mb'` → `10485760`, `'1.5kb'` → `1536` (fractions
|
|
316
|
+
* are rounded down). Returns `undefined` for anything else (negative or fractional numbers,
|
|
317
|
+
* unknown units, `NaN`).
|
|
318
|
+
*/
|
|
319
|
+
declare function parseByteSize(value: unknown): number | undefined;
|
|
320
|
+
/** Human-readable size for messages: `5 MiB`, `1.5 KiB`, `512 bytes`, `unlimited`. */
|
|
321
|
+
declare function formatByteSize(bytes: number): string;
|
|
322
|
+
/** Options of `@UploadedFile` (static: evaluated at build time). */
|
|
323
|
+
interface UploadedFileOptions {
|
|
324
|
+
/**
|
|
325
|
+
* Answer 400 when no file is sent. Default: `true`, unless the parameter is optional
|
|
326
|
+
* (`file?: File`, `File | undefined`).
|
|
327
|
+
*/
|
|
328
|
+
readonly required?: boolean;
|
|
329
|
+
/** Maximum size of the file (`5_000_000`, `'5mb'`; `kb` = 1024 bytes). Default: plugin `uploads.maxFileSize`. */
|
|
330
|
+
readonly maxSize?: ByteSize;
|
|
331
|
+
/**
|
|
332
|
+
* Accepted media types, as declared by the client: `'image/png'`, `'image/*'`, `'*\/*'`. Other
|
|
333
|
+
* types answer 415. Default: any.
|
|
334
|
+
*/
|
|
335
|
+
readonly mimeTypes?: readonly string[];
|
|
336
|
+
}
|
|
337
|
+
/** Options of `@UploadedFiles` (static: evaluated at build time). */
|
|
338
|
+
interface UploadedFilesOptions extends UploadedFileOptions {
|
|
339
|
+
/** Maximum number of files (413 above). Default: plugin `uploads.maxFiles`. */
|
|
340
|
+
readonly maxFiles?: number;
|
|
341
|
+
}
|
|
342
|
+
/** Plugin-wide upload limits (`HttpPlugin.configure({ uploads })`). */
|
|
343
|
+
interface UploadLimits {
|
|
344
|
+
/** Default maximum size of one file (`maxSize` of upload parameters). Default: `'10mb'`. */
|
|
345
|
+
readonly maxFileSize?: ByteSize;
|
|
346
|
+
/** Default maximum number of files of `@UploadedFiles` (and of multipart bodies of other routes). Default: `10`. */
|
|
347
|
+
readonly maxFiles?: number;
|
|
348
|
+
/** Maximum number of parts (text fields and files) of a multipart body (413 above). Default: `1000`. */
|
|
349
|
+
readonly maxParts?: number;
|
|
350
|
+
/** Maximum size of one text field of a multipart body (413 above). Default: `'1mb'`. */
|
|
351
|
+
readonly maxFieldSize?: ByteSize;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Reads uploaded files from the context (`ctx.files`) in generated `invoke` functions
|
|
355
|
+
* (`@UploadedFile('avatar') avatar: File` → `uploads.file(ctx, 'avatar')`). `field` `undefined`
|
|
356
|
+
* means any field. Limits were enforced while the body was read; these helpers check presence.
|
|
357
|
+
*/
|
|
358
|
+
interface Uploads {
|
|
359
|
+
/** The file of `field`; 400 `Missing file "field"` when absent. */
|
|
360
|
+
file(ctx: HttpContext, field: string | undefined): File;
|
|
361
|
+
/** The file of `field`, `undefined` when absent. */
|
|
362
|
+
optionalFile(ctx: HttpContext, field: string | undefined): File | undefined;
|
|
363
|
+
/** The files of `field` (at least one); 400 `Missing files "field"` when none. */
|
|
364
|
+
files(ctx: HttpContext, field: string | undefined): File[];
|
|
365
|
+
/** The files of `field`, possibly none. */
|
|
366
|
+
optionalFiles(ctx: HttpContext, field: string | undefined): File[];
|
|
367
|
+
}
|
|
368
|
+
/** The helpers of the generated code (`import { uploads } from '@znight/http'`). */
|
|
369
|
+
declare const uploads: Uploads;
|
|
370
|
+
declare const requestIdBrand: unique symbol;
|
|
371
|
+
/** Why `ctx.signal` aborted: the client left, the server shuts down, or an SSE stream reached its `maxDuration`. */
|
|
372
|
+
type RequestAbortKind = "client" | "shutdown" | "expired";
|
|
373
|
+
/**
|
|
374
|
+
* The reason of an aborted `ctx.signal`. Named `AbortError`, what `fetch`, `node:timers/promises`
|
|
375
|
+
* and most libraries check.
|
|
376
|
+
*/
|
|
377
|
+
declare class RequestAbortedError extends Error {
|
|
378
|
+
override readonly name = "AbortError";
|
|
379
|
+
/** `client`: disconnected; `shutdown`: the server stops; `expired`: an SSE stream reached its `maxDuration`. */
|
|
380
|
+
readonly kind: RequestAbortKind;
|
|
381
|
+
constructor(kind: RequestAbortKind);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Identifier of the current request: the incoming `x-request-id` header (see the plugin option
|
|
385
|
+
* `requestIdHeader`) or a generated UUID. Injectable in request-scoped providers.
|
|
386
|
+
*/
|
|
387
|
+
type RequestId = string & {
|
|
388
|
+
readonly [requestIdBrand]: true;
|
|
389
|
+
};
|
|
390
|
+
/** The route that matched a request. */
|
|
391
|
+
interface HttpRouteInfo {
|
|
392
|
+
/** Route method (`GET`...; the concrete method for `@All` routes). */
|
|
393
|
+
readonly method: string;
|
|
394
|
+
/** Route pattern including the plugin prefix (`/api/users/:id`). */
|
|
395
|
+
readonly path: string;
|
|
396
|
+
/** Controller provider token. */
|
|
397
|
+
readonly controller: Token;
|
|
398
|
+
/** Handler method name. */
|
|
399
|
+
readonly handler: string;
|
|
400
|
+
/**
|
|
401
|
+
* Effective route metadata (`createRouteDecorator`): the controller's entries, except those of a
|
|
402
|
+
* group the route decides itself, then the route's entries (the nearest decision wins).
|
|
403
|
+
*/
|
|
404
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
405
|
+
}
|
|
406
|
+
declare module "@znight/core" {
|
|
407
|
+
/** Custom parameter decorators (`createParamDecorator`) resolve HTTP parameters from the {@link HttpContext}. */
|
|
408
|
+
interface ParamContexts {
|
|
409
|
+
http: HttpContext;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Adapter-neutral view of the current HTTP request, shared by guards, interceptors, exception
|
|
414
|
+
* mappers and handlers (`@Ctx()`), and injectable in request-scoped providers.
|
|
415
|
+
*/
|
|
416
|
+
interface HttpContext {
|
|
417
|
+
readonly requestId: RequestId;
|
|
418
|
+
/** Upper-case method. */
|
|
419
|
+
readonly method: string;
|
|
420
|
+
/** Raw request target (`/users/a%20b?page=2`). */
|
|
421
|
+
readonly url: string;
|
|
422
|
+
/** Raw path, without query string (`/users/a%20b`). */
|
|
423
|
+
readonly path: string;
|
|
424
|
+
/** Decoded route parameters (`*` holds the part matched by a trailing wildcard). */
|
|
425
|
+
readonly params: Readonly<Record<string, string>>;
|
|
426
|
+
/** Decoded query string (repeated keys → arrays), parsed on first access. */
|
|
427
|
+
readonly query: Readonly<QueryRecord>;
|
|
428
|
+
/** Request headers, lower-case names. */
|
|
429
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
430
|
+
/**
|
|
431
|
+
* Parsed body. Populated before the interceptors for routes declaring `@Body()` or upload
|
|
432
|
+
* parameters (and after {@link parseBody} resolved otherwise); `undefined` until then and for
|
|
433
|
+
* empty bodies. Forms (urlencoded and multipart) are records of their text fields.
|
|
434
|
+
*/
|
|
435
|
+
readonly body: unknown;
|
|
436
|
+
/**
|
|
437
|
+
* Files of a `multipart/form-data` body by field name, in body order (standard `File` objects,
|
|
438
|
+
* held in memory). Populated with {@link body}; empty for other bodies.
|
|
439
|
+
*/
|
|
440
|
+
readonly files: Readonly<Record<string, readonly File[]>>;
|
|
441
|
+
/** The matched route, `undefined` for unmatched requests (404/405). */
|
|
442
|
+
readonly route: HttpRouteInfo | undefined;
|
|
443
|
+
/** Per-request storage for guards, interceptors and handlers (`ctx.state.set('user', user)`). */
|
|
444
|
+
readonly state: Map<string | symbol, unknown>;
|
|
445
|
+
/** Native request of the adapter (`IncomingMessage`, `FastifyRequest`, Express `Request`, Web `Request`). */
|
|
446
|
+
readonly request: unknown;
|
|
447
|
+
/** Native response of the adapter (`ServerResponse`, `FastifyReply`, Express `Response`, Hono `Context`). */
|
|
448
|
+
readonly response: unknown;
|
|
449
|
+
/** Adapter name (`node`, `fastify`, `express`, `hono`). */
|
|
450
|
+
readonly adapter: string;
|
|
451
|
+
/**
|
|
452
|
+
* Client address, when the adapter knows it. With the plugin option `trustProxy`, the address
|
|
453
|
+
* found in `x-forwarded-for` behind the trusted proxies; otherwise the adapter's address (the
|
|
454
|
+
* socket peer, or what Fastify / Express derive from their own proxy settings).
|
|
455
|
+
*/
|
|
456
|
+
readonly ip: string | undefined;
|
|
457
|
+
/**
|
|
458
|
+
* `https` when the connection uses TLS, or, behind a trusted proxy (`trustProxy`), when the
|
|
459
|
+
* leftmost `x-forwarded-proto` says so; `http` otherwise.
|
|
460
|
+
*/
|
|
461
|
+
readonly protocol: "http" | "https";
|
|
462
|
+
/** Headers added to the response, whatever its outcome (errors included). */
|
|
463
|
+
readonly responseHeaders: Readonly<Record<string, HeaderValue>>;
|
|
464
|
+
/** A request header (case-insensitive name). */
|
|
465
|
+
header(name: string): string | undefined;
|
|
466
|
+
/** Sets a response header (case-insensitive name), sent with success and error responses. */
|
|
467
|
+
setHeader(name: string, value: HeaderValue): void;
|
|
468
|
+
/** Appends a response header value (`set-cookie`...). */
|
|
469
|
+
appendHeader(name: string, value: string): void;
|
|
470
|
+
/**
|
|
471
|
+
* Request cookies (`cookie` header), parsed on first access: decoded values, the first occurrence
|
|
472
|
+
* of a name wins. A prototype-less, frozen record.
|
|
473
|
+
*/
|
|
474
|
+
readonly cookies: Readonly<Record<string, string>>;
|
|
475
|
+
/**
|
|
476
|
+
* The verified value of a signed cookie (`@SignedCookie`), `undefined` when it is absent, tampered
|
|
477
|
+
* or signed with a key that is no longer in `cookies.secrets`. Verified once per name.
|
|
478
|
+
*
|
|
479
|
+
* @throws Error when the plugin option `cookies.secrets` is not set.
|
|
480
|
+
*/
|
|
481
|
+
signedCookie(name: string): string | undefined;
|
|
482
|
+
/**
|
|
483
|
+
* Sets a cookie on the response, whatever its outcome (errors, redirects included), with the
|
|
484
|
+
* secure defaults (`Path=/; Secure; HttpOnly; SameSite=Lax`, see the plugin option
|
|
485
|
+
* `cookies.defaults`). A later cookie with the same name, domain and path replaces this one; a
|
|
486
|
+
* cookie of the same key in an `HttpResponse` (`withCookie`) wins over it. Everything is validated
|
|
487
|
+
* now (name, value, attributes, prefixes, size), so the stack trace points to the faulty call.
|
|
488
|
+
*
|
|
489
|
+
* @throws TypeError / RangeError for an invalid cookie; Error once the response headers were sent
|
|
490
|
+
* (a running event stream).
|
|
491
|
+
*/
|
|
492
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
493
|
+
/**
|
|
494
|
+
* Clears a cookie (`Max-Age=0`, with the same domain and path, and the defaults: a `__Host-`
|
|
495
|
+
* cookie is cleared with valid attributes).
|
|
496
|
+
*
|
|
497
|
+
* @throws like {@link setCookie}.
|
|
498
|
+
*/
|
|
499
|
+
clearCookie(name: string, options?: ClearCookieOptions): void;
|
|
500
|
+
/** Cookies set on this response so far (`setCookie` / `clearCookie`), in order. */
|
|
501
|
+
readonly responseCookies: readonly ResponseCookie[];
|
|
502
|
+
/**
|
|
503
|
+
* Aborted when the client disconnects before the response was written, when the response body is
|
|
504
|
+
* cancelled, or when the server closes an event stream (shutdown, `maxDuration`); never after a
|
|
505
|
+
* normal completion. The reason is a {@link RequestAbortedError}. Pass it to the work the request
|
|
506
|
+
* starts (`fetch(url, { signal: ctx.signal })`, `setTimeout(ms, v, { signal })`). Created on first
|
|
507
|
+
* access.
|
|
508
|
+
*/
|
|
509
|
+
readonly signal: AbortSignal;
|
|
510
|
+
/**
|
|
511
|
+
* Reads and parses the body (once): JSON (`application/json`, `+json`), text (`text/*`), forms
|
|
512
|
+
* (`application/x-www-form-urlencoded` and `multipart/form-data` → record of the text fields,
|
|
513
|
+
* repeated names as arrays; files in {@link files}), raw bytes otherwise; `undefined` when empty.
|
|
514
|
+
* Fails with 400 (malformed), 413 (body or upload limits) or 415 (unsupported encoding, refused
|
|
515
|
+
* file type, non-multipart body on an upload route).
|
|
516
|
+
*/
|
|
517
|
+
parseBody(): Promise<unknown>;
|
|
518
|
+
/** The raw body bytes (once; shared with {@link parseBody}), e.g. to check a webhook signature. */
|
|
519
|
+
rawBody(): Promise<Uint8Array>;
|
|
520
|
+
/**
|
|
521
|
+
* The request body as a Web stream, read as it arrives (nothing is buffered): the escape hatch
|
|
522
|
+
* for large uploads (`PUT /files/:name` piped to disk or object storage). The body limit of the
|
|
523
|
+
* route still applies (`@BodyLimit`, `Infinity` to disable): past it the stream errors with a
|
|
524
|
+
* `PayloadTooLargeException` (413). Exclusive with {@link rawBody} / {@link parseBody}: throws when
|
|
525
|
+
* the body was already read (routes with `@Body()` or upload parameters read it before the
|
|
526
|
+
* handler).
|
|
527
|
+
*/
|
|
528
|
+
bodyStream(): ReadableStream<Uint8Array>;
|
|
529
|
+
}
|
|
530
|
+
/** Route methods. `ALL` registers the route for GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS. */
|
|
531
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL";
|
|
532
|
+
/**
|
|
533
|
+
* Where a handler parameter comes from (`file` / `files`: `@UploadedFile` / `@UploadedFiles`, the
|
|
534
|
+
* `name` being the multipart field; `cookie` / `signedCookie`: `@Cookie` / `@SignedCookie`, the
|
|
535
|
+
* `name` being the cookie name).
|
|
536
|
+
*/
|
|
537
|
+
type HttpParamSource = "param" | "query" | "body" | "headers" | "request" | "response" | "context" | "file" | "files" | "cookie" | "signedCookie" | "custom";
|
|
538
|
+
/** A handler parameter (informational: extraction, coercion and validation happen in `invoke`). */
|
|
539
|
+
interface HttpParamDefinition {
|
|
540
|
+
/** Position in the handler signature. */
|
|
541
|
+
readonly index: number;
|
|
542
|
+
readonly source: HttpParamSource;
|
|
543
|
+
/** Parameter / query key / header name (lower case) / multipart field. Absent: the whole object (every field). */
|
|
544
|
+
readonly name?: string;
|
|
545
|
+
/** Serialised declared type (`number`, `string[]`, `'asc' | 'desc'`...). */
|
|
546
|
+
readonly type?: string;
|
|
547
|
+
readonly optional?: boolean;
|
|
548
|
+
/** `true` when the value is validated with a schema. */
|
|
549
|
+
readonly schema?: boolean;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Files accepted by an upload parameter (`@UploadedFile` / `@UploadedFiles`). The plugin enforces
|
|
553
|
+
* these rules while the multipart body is read; the presence of required files is checked by
|
|
554
|
+
* `invoke` (the `uploads` helpers).
|
|
555
|
+
*/
|
|
556
|
+
interface HttpUploadDefinition {
|
|
557
|
+
/** Multipart field. Absent: files of any field. */
|
|
558
|
+
readonly field?: string;
|
|
559
|
+
/** `true` for `@UploadedFiles` (several files). */
|
|
560
|
+
readonly multiple?: boolean;
|
|
561
|
+
/** `true` when a file must be sent (informational, checked by `invoke`). */
|
|
562
|
+
readonly required?: boolean;
|
|
563
|
+
/** Maximum size of each file in bytes (`Infinity`: no limit). Default: plugin `uploads.maxFileSize`. */
|
|
564
|
+
readonly maxSize?: number;
|
|
565
|
+
/** Maximum number of files (`@UploadedFiles`). Default: plugin `uploads.maxFiles` (1 for `@UploadedFile`). */
|
|
566
|
+
readonly maxFiles?: number;
|
|
567
|
+
/** Accepted media types (`image/png`, `image/*`), compared with the type declared by the client. */
|
|
568
|
+
readonly mimeTypes?: readonly string[];
|
|
569
|
+
}
|
|
570
|
+
/** Options of an `@Sse` route (`HttpRouteDefinition.sse`). */
|
|
571
|
+
interface HttpSseDefinition {
|
|
572
|
+
/** Heartbeat interval in ms (≥ 1 000); `0` disables it. Absent: the plugin option `sse.heartbeat`. */
|
|
573
|
+
readonly heartbeat?: number;
|
|
574
|
+
/** `retry` sent first (ms). Absent: the plugin option `sse.retry`. */
|
|
575
|
+
readonly retry?: number;
|
|
576
|
+
/** The stream ends gracefully after this many ms (≥ 1 000). */
|
|
577
|
+
readonly maxDuration?: number;
|
|
578
|
+
}
|
|
579
|
+
/** A route: one decorated method of a controller. */
|
|
580
|
+
interface HttpRouteDefinition {
|
|
581
|
+
readonly method: HttpMethod;
|
|
582
|
+
/** Path of the route decorator, normalised (`/:id`). */
|
|
583
|
+
readonly path: string;
|
|
584
|
+
/** `joinPaths(controller.basePath, path)` (without the plugin `prefix`). */
|
|
585
|
+
readonly fullPath: string;
|
|
586
|
+
/** Name of the handler method. */
|
|
587
|
+
readonly handler: string;
|
|
588
|
+
/** `@HttpCode(status)`. */
|
|
589
|
+
readonly status?: number;
|
|
590
|
+
/** `@Header(name, value)` entries (names in lower case). */
|
|
591
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
592
|
+
/**
|
|
593
|
+
* Route metadata (`createRouteDecorator`), merged over the controller metadata at runtime, one
|
|
594
|
+
* decision per group: the route's entry of a group replaces the controller's entries of that group
|
|
595
|
+
* (see {@link HttpMetadata.groups}).
|
|
596
|
+
*/
|
|
597
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
598
|
+
/** Route guards (provider tokens), run after global and controller guards. */
|
|
599
|
+
readonly guards: readonly Token[];
|
|
600
|
+
/** Route interceptors (provider tokens), innermost. */
|
|
601
|
+
readonly interceptors: readonly Token[];
|
|
602
|
+
readonly params: readonly HttpParamDefinition[];
|
|
603
|
+
/**
|
|
604
|
+
* `@BodyLimit` (route or controller): maximum body size in bytes (`Infinity`: no limit). Default:
|
|
605
|
+
* the plugin `bodyLimit`, or for upload routes the sum of the file limits plus `bodyLimit`.
|
|
606
|
+
*/
|
|
607
|
+
readonly bodyLimit?: number;
|
|
608
|
+
/**
|
|
609
|
+
* Upload rules (one per `@UploadedFile` / `@UploadedFiles` parameter). Routes with uploads parse
|
|
610
|
+
* `multipart/form-data` bodies before the interceptors and answer 415 to other non-empty bodies.
|
|
611
|
+
*/
|
|
612
|
+
readonly uploads?: readonly HttpUploadDefinition[];
|
|
613
|
+
/** `true` when the handler takes `@Res()` and writes the response itself. */
|
|
614
|
+
readonly rawResponse?: boolean;
|
|
615
|
+
/**
|
|
616
|
+
* `@Sse` route (GET only): the handler result (an `AsyncIterable`, an `Iterable` or a
|
|
617
|
+
* `ReadableStream` of `SseEvent`) is served as an event stream; an `HttpResponse` or a Web
|
|
618
|
+
* `Response` is sent as usual. `{}` without options.
|
|
619
|
+
*/
|
|
620
|
+
readonly sse?: HttpSseDefinition;
|
|
621
|
+
/**
|
|
622
|
+
* Provider classes resolving custom parameters (`ParamResolver`, RFC 0010), in order of first use:
|
|
623
|
+
* the plugin resolves them in the request scope and passes the instances to `invoke` as `x`
|
|
624
|
+
* (`x[i]` is the instance of `paramResolvers[i]`). Absent when the route has none.
|
|
625
|
+
*/
|
|
626
|
+
readonly paramResolvers?: readonly Token[];
|
|
627
|
+
/**
|
|
628
|
+
* Calls the handler: extracts, coerces and validates every argument from the context (custom
|
|
629
|
+
* parameters included), then calls the method on the controller instance. May return a promise.
|
|
630
|
+
* `resolvers` is passed when the route has {@link paramResolvers}.
|
|
631
|
+
*/
|
|
632
|
+
readonly invoke: (controller: any, ctx: HttpContext, resolvers?: any) => unknown;
|
|
633
|
+
}
|
|
634
|
+
/** A controller: a provider whose methods are routes. */
|
|
635
|
+
interface HttpControllerDefinition {
|
|
636
|
+
/** Provider token of the controller class. */
|
|
637
|
+
readonly token: Token;
|
|
638
|
+
/** Normalised base path (`/users`, `/` when absent). */
|
|
639
|
+
readonly basePath: string;
|
|
640
|
+
/** Controller guards (provider tokens). */
|
|
641
|
+
readonly guards: readonly Token[];
|
|
642
|
+
/** Controller interceptors (provider tokens). */
|
|
643
|
+
readonly interceptors: readonly Token[];
|
|
644
|
+
/** Controller metadata (`createRouteDecorator` on the class). */
|
|
645
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
646
|
+
readonly routes: readonly HttpRouteDefinition[];
|
|
647
|
+
}
|
|
648
|
+
/** A route decorator group (`createRouteDecorator(key, { group, required })`). */
|
|
649
|
+
interface HttpRouteGroupDefinition {
|
|
650
|
+
/** Keys of the decorators of the group, sorted. */
|
|
651
|
+
readonly keys: readonly string[];
|
|
652
|
+
/** Every route must carry a decision of the group. */
|
|
653
|
+
readonly required: boolean;
|
|
654
|
+
}
|
|
655
|
+
/** `metadata.extensions.http`. */
|
|
656
|
+
interface HttpMetadata {
|
|
657
|
+
readonly controllers: readonly HttpControllerDefinition[];
|
|
658
|
+
/**
|
|
659
|
+
* Route decorator groups that need to be known at runtime (required, holding several keys, or
|
|
660
|
+
* named differently from their key): group → `{ keys, required }`. A key belongs to one group at
|
|
661
|
+
* most; a key listed nowhere is its own group. Absent when no group needs it.
|
|
662
|
+
*/
|
|
663
|
+
readonly groups?: Readonly<Record<string, HttpRouteGroupDefinition>>;
|
|
664
|
+
}
|
|
665
|
+
declare module "@znight/core" {
|
|
666
|
+
interface ZnightExtensionsMetadata {
|
|
667
|
+
readonly http?: HttpMetadata;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** How a file is presented to the client (`Content-Disposition`). */
|
|
671
|
+
type ContentDispositionType = "attachment" | "inline";
|
|
672
|
+
/** Options of `HttpResponse.file()`. */
|
|
673
|
+
interface FileResponseOptions {
|
|
674
|
+
/**
|
|
675
|
+
* Name proposed to the client in `Content-Disposition` (any Unicode name; path separators are
|
|
676
|
+
* removed). Setting it makes the response an `attachment` unless `disposition` says otherwise.
|
|
677
|
+
*/
|
|
678
|
+
readonly filename?: string;
|
|
679
|
+
/**
|
|
680
|
+
* Media type of the response. Default: the type of the `Blob`, else looked up from the extension
|
|
681
|
+
* of `filename` / of the path (small built-in table, text types with `charset=utf-8`), else
|
|
682
|
+
* `application/octet-stream`.
|
|
683
|
+
*/
|
|
684
|
+
readonly contentType?: string;
|
|
685
|
+
/**
|
|
686
|
+
* `attachment` (download) or `inline` (displayed by the browser). Default: `attachment` when
|
|
687
|
+
* `filename` is set, otherwise no `Content-Disposition` header. Without `filename`, the file name
|
|
688
|
+
* (path basename, `File.name`) is used.
|
|
689
|
+
*/
|
|
690
|
+
readonly disposition?: ContentDispositionType;
|
|
691
|
+
/**
|
|
692
|
+
* Directory the path is resolved in. The resolved file (symbolic links followed) must stay inside
|
|
693
|
+
* it: `..` segments, absolute paths and links escaping the directory answer 404. Always set it
|
|
694
|
+
* when the path comes from the request.
|
|
695
|
+
*/
|
|
696
|
+
readonly root?: string;
|
|
697
|
+
/**
|
|
698
|
+
* With `root`: whether files or directories whose name starts with a dot (`.env`, `.git/config`)
|
|
699
|
+
* may be sent. Default: `'deny'` (404).
|
|
700
|
+
*/
|
|
701
|
+
readonly dotfiles?: "allow" | "deny";
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* The body of `HttpResponse.file()`: a file on disk or a `Blob`, resolved by the pipeline when the
|
|
705
|
+
* response is sent (stat, conditional request, range).
|
|
706
|
+
*/
|
|
707
|
+
declare class FileBody {
|
|
708
|
+
/** Path (`string` / `file:` URL) or `Blob` / `File`. */
|
|
709
|
+
readonly source: string | URL | Blob;
|
|
710
|
+
readonly options: FileResponseOptions;
|
|
711
|
+
constructor(source: string | URL | Blob, options?: FileResponseOptions);
|
|
712
|
+
}
|
|
713
|
+
/** An inclusive byte range. */
|
|
714
|
+
interface ByteRange {
|
|
715
|
+
readonly start: number;
|
|
716
|
+
readonly end: number;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Parses a `Range` header for a representation of `size` bytes. Only a single `bytes` range is
|
|
720
|
+
* honoured: other units, several ranges and malformed values return `undefined` (the whole
|
|
721
|
+
* representation is sent, as RFC 9110 allows); a valid range starting after the end returns
|
|
722
|
+
* `'unsatisfiable'` (416).
|
|
723
|
+
*/
|
|
724
|
+
declare function parseRange(header: string, size: number): ByteRange | "unsatisfiable" | undefined;
|
|
725
|
+
/**
|
|
726
|
+
* Builds a `Content-Disposition` header value (RFC 6266): `attachment; filename="report.pdf"`, with
|
|
727
|
+
* an RFC 8187 / 5987 `filename*` parameter for names that are not plain ASCII
|
|
728
|
+
* (`attachment; filename="r_sum_.pdf"; filename*=UTF-8''r%C3%A9sum%C3%A9.pdf`). Directory parts and
|
|
729
|
+
* control characters of `filename` are removed.
|
|
730
|
+
*/
|
|
731
|
+
declare function contentDisposition(filename?: string, type?: ContentDispositionType): string;
|
|
732
|
+
/** One event of an event stream. */
|
|
733
|
+
interface SseEvent<T = unknown> {
|
|
734
|
+
/** A string is sent as is; anything else goes through `JSON.stringify` (`toJSON` honoured). */
|
|
735
|
+
readonly data?: T;
|
|
736
|
+
/** Event type (`addEventListener(type)`). The client defaults to `message`. No CR or LF. */
|
|
737
|
+
readonly event?: string;
|
|
738
|
+
/** Last event id, echoed by the client as `Last-Event-ID`. No CR, LF or NUL. `''` resets it. */
|
|
739
|
+
readonly id?: string | number;
|
|
740
|
+
/** Reconnection delay in ms (a safe integer ≥ 0). */
|
|
741
|
+
readonly retry?: number;
|
|
742
|
+
/** Comment lines, ignored by `EventSource` (debugging, padding). */
|
|
743
|
+
readonly comment?: string;
|
|
744
|
+
}
|
|
745
|
+
/** Options of `@Sse()` and `HttpResponse.sse()`. */
|
|
746
|
+
interface SseOptions {
|
|
747
|
+
/**
|
|
748
|
+
* Milliseconds without an event after which a keep-alive comment (`:`) is sent (an integer
|
|
749
|
+
* from 1 000 to 2 147 483 647). `false` disables it. Default: the plugin option `sse.heartbeat`
|
|
750
|
+
* (8 000).
|
|
751
|
+
*/
|
|
752
|
+
readonly heartbeat?: number | false;
|
|
753
|
+
/**
|
|
754
|
+
* Client reconnection delay (`retry:`, ms), sent first. Default: the plugin option `sse.retry`
|
|
755
|
+
* (none: browsers use about 3 s).
|
|
756
|
+
*/
|
|
757
|
+
readonly retry?: number;
|
|
758
|
+
/**
|
|
759
|
+
* The server ends the stream gracefully after this many ms (load rebalancing, re-authentication):
|
|
760
|
+
* `ctx.signal` aborts (`'expired'`), then the client gets a jittered `retry` and reconnects. An
|
|
761
|
+
* integer from 1 000 to 2 147 483 647 (about 24.8 days, the longest timer). Default: none.
|
|
762
|
+
*/
|
|
763
|
+
readonly maxDuration?: number;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* What `@Sse` handlers and `HttpResponse.sse()` accept: an async iterable (async generators,
|
|
767
|
+
* `EventChannel`, a Node.js `Readable` of raw bytes), an iterable (arrays, sync generators; not
|
|
768
|
+
* strings) or a `ReadableStream`, of `SseEvent` objects or `Uint8Array` chunks (already-framed SSE
|
|
769
|
+
* text, written as is: proxying an upstream event stream).
|
|
770
|
+
*/
|
|
771
|
+
type SseSource = AsyncIterable<SseEvent | Uint8Array> | Iterable<SseEvent> | ReadableStream<SseEvent | Uint8Array>;
|
|
772
|
+
/** The body of `HttpResponse.sse()`: an event source and its options (served by the plugin). */
|
|
773
|
+
declare class SseBody {
|
|
774
|
+
readonly source: SseSource;
|
|
775
|
+
readonly options: SseOptions;
|
|
776
|
+
constructor(source: SseSource, options?: SseOptions);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Encodes one event (WHATWG framing): one `: <line>` per comment line, `id:`, `event:`, `retry:`,
|
|
780
|
+
* then one `data: <line>` per line of the data (split on CRLF, CR and LF, so data can never inject
|
|
781
|
+
* fields or end the event early), and an empty line. An `event` without `data` gets an empty
|
|
782
|
+
* `data:` line (otherwise `EventSource` would not dispatch it); events with only `id`, `retry` or
|
|
783
|
+
* `comment` have no data line.
|
|
784
|
+
*
|
|
785
|
+
* ```ts
|
|
786
|
+
* encodeSseEvent({ id: 41, event: 'status', data: { status: 'paid' } })
|
|
787
|
+
* // 'id: 41\nevent: status\ndata: {"status":"paid"}\n\n'
|
|
788
|
+
* ```
|
|
789
|
+
*
|
|
790
|
+
* @throws TypeError for anything but a plain object with the keys of {@link SseEvent} (strings,
|
|
791
|
+
* numbers, `null`: yield `{ data: value }`), an empty event, an `event` / `id` with CR or LF (NUL
|
|
792
|
+
* for `id`), a `retry` that is not an integer ≥ 0, a `data` JSON cannot serialise.
|
|
793
|
+
*/
|
|
794
|
+
declare function encodeSseEvent(event: SseEvent): string;
|
|
795
|
+
/**
|
|
796
|
+
* A stream source accepted by `HttpResponse.stream()`: a Web `ReadableStream`, a Node.js `Readable`
|
|
797
|
+
* or any async iterable (async generator...) of `Uint8Array` / `string` chunks (strings are encoded
|
|
798
|
+
* as UTF-8).
|
|
799
|
+
*/
|
|
800
|
+
type StreamSource = ReadableStream<Uint8Array | string> | AsyncIterable<Uint8Array | string>;
|
|
801
|
+
/** Options of {@link HttpResponse.stream}. */
|
|
802
|
+
interface StreamResponseOptions {
|
|
803
|
+
/** Media type. Default: from the extension of `filename`, else `application/octet-stream`. */
|
|
804
|
+
readonly contentType?: string;
|
|
805
|
+
/** Name proposed to the client (`Content-Disposition: attachment` unless `disposition` says otherwise). */
|
|
806
|
+
readonly filename?: string;
|
|
807
|
+
/** `attachment` or `inline`. Default: `attachment` when `filename` is set, otherwise no header. */
|
|
808
|
+
readonly disposition?: ContentDispositionType;
|
|
809
|
+
}
|
|
810
|
+
/** Options of {@link HttpResponse}. */
|
|
811
|
+
interface HttpResponseInit {
|
|
812
|
+
/** Status code. Default: the route default (`@HttpCode`, 201 for POST, 204 without body, 200). */
|
|
813
|
+
readonly status?: number;
|
|
814
|
+
/** Response headers (names are case-insensitive; arrays send the header several times). */
|
|
815
|
+
readonly headers?: Readonly<Record<string, HeaderValue>>;
|
|
816
|
+
/** Cookies to set (see {@link HttpResponse.withCookie}), in order. */
|
|
817
|
+
readonly cookies?: readonly ResponseCookie[];
|
|
818
|
+
}
|
|
819
|
+
/** Redirect statuses accepted by {@link HttpResponse.redirect}. */
|
|
820
|
+
type RedirectStatus = 301 | 302 | 303 | 307 | 308;
|
|
821
|
+
/**
|
|
822
|
+
* A handler result carrying a status and headers next to the body. The body is serialised with the
|
|
823
|
+
* usual rules (objects → JSON, strings → text, bytes and streams → octet-stream).
|
|
824
|
+
*
|
|
825
|
+
* ```ts
|
|
826
|
+
* @Post('/')
|
|
827
|
+
* async create(@Body(CreateUser) input: CreateUser): Promise<HttpResponse<User>> {
|
|
828
|
+
* const user = await this.users.create(input)
|
|
829
|
+
* return HttpResponse.created(user, `/users/${user.id}`)
|
|
830
|
+
* }
|
|
831
|
+
* ```
|
|
832
|
+
*
|
|
833
|
+
* Status precedence: `@HttpCode` > `HttpResponse.status` > route default.
|
|
834
|
+
*/
|
|
835
|
+
declare class HttpResponse<T = unknown> {
|
|
836
|
+
#private;
|
|
837
|
+
readonly body: T;
|
|
838
|
+
readonly status: number | undefined;
|
|
839
|
+
readonly headers: Readonly<Record<string, HeaderValue>>;
|
|
840
|
+
/** Cookies set by this response (`withCookie` / `withoutCookie`), in order; they win over `ctx.setCookie()`. */
|
|
841
|
+
readonly cookies: readonly ResponseCookie[];
|
|
842
|
+
constructor(body: T, init?: HttpResponseInit);
|
|
843
|
+
/** `200 OK` with `body`. */
|
|
844
|
+
static ok<T>(body: T, headers?: Readonly<Record<string, HeaderValue>>): HttpResponse<T>;
|
|
845
|
+
/**
|
|
846
|
+
* `201 Created`, with a `location` header when given. With a body, the response is typed with it
|
|
847
|
+
* (`Promise<HttpResponse<BookResponse>>` type-checks).
|
|
848
|
+
*/
|
|
849
|
+
static created(): HttpResponse<undefined>;
|
|
850
|
+
static created<T>(body: T, location?: string): HttpResponse<T>;
|
|
851
|
+
/** `202 Accepted`, typed with its body when there is one. */
|
|
852
|
+
static accepted(): HttpResponse<undefined>;
|
|
853
|
+
static accepted<T>(body: T): HttpResponse<T>;
|
|
854
|
+
/** `204 No Content`. */
|
|
855
|
+
static noContent(): HttpResponse<undefined>;
|
|
856
|
+
/** A redirect (`302 Found` by default) to `location`. */
|
|
857
|
+
static redirect(location: string, status?: RedirectStatus): HttpResponse<undefined>;
|
|
858
|
+
/**
|
|
859
|
+
* Sends a file from disk (`string` path or `file:` URL) or a `Blob` / `File`, streamed with
|
|
860
|
+
* `Content-Length`, `Content-Type` (from the extension, see `contentTypeFor`), `Last-Modified`,
|
|
861
|
+
* `ETag` and `Accept-Ranges`. Conditional requests answer 304 / 412 and single byte ranges 206
|
|
862
|
+
* (416 when unsatisfiable). A missing file answers 404.
|
|
863
|
+
*
|
|
864
|
+
* ```ts
|
|
865
|
+
* @Get('/reports/:name')
|
|
866
|
+
* download(@Param('name') name: string) {
|
|
867
|
+
* // `root` confines the path: '../../etc/passwd' answers 404
|
|
868
|
+
* return HttpResponse.file(name, { root: REPORTS_DIR, filename: `rapport ${name}` })
|
|
869
|
+
* }
|
|
870
|
+
* ```
|
|
871
|
+
*
|
|
872
|
+
* Never pass a path built from the request without `root`.
|
|
873
|
+
*/
|
|
874
|
+
static file(file: string | URL | Blob, options?: FileResponseOptions): HttpResponse<FileBody>;
|
|
875
|
+
/**
|
|
876
|
+
* Streams a Web `ReadableStream`, a Node.js `Readable` or an async iterable (async generator) of
|
|
877
|
+
* `Uint8Array` / `string` chunks. The source is read as the client receives the data
|
|
878
|
+
* (backpressure) and cancelled when the client disconnects; it runs in the request scope.
|
|
879
|
+
*
|
|
880
|
+
* ```ts
|
|
881
|
+
* @Get('/export.csv')
|
|
882
|
+
* export() {
|
|
883
|
+
* return HttpResponse.stream(this.rows.csvLines(), { filename: 'export.csv' }) // text/csv; charset=utf-8
|
|
884
|
+
* }
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
static stream(source: StreamSource, options?: StreamResponseOptions): HttpResponse<ReadableStream<Uint8Array>>;
|
|
888
|
+
/**
|
|
889
|
+
* An event stream (Server-Sent Events) on any route and any method: `200` unless `@HttpCode` or
|
|
890
|
+
* {@link withStatus} say otherwise (`EventSource` requires 200; `fetch()` clients read any status),
|
|
891
|
+
* even on `POST` routes. Same source, framing and lifecycle as an `@Sse()` route.
|
|
892
|
+
*
|
|
893
|
+
* ```ts
|
|
894
|
+
* @Post('/:id/assistant')
|
|
895
|
+
* assistant(@Param('id') id: string, @Body(Prompt) prompt: Prompt, @Ctx() ctx: HttpContext) {
|
|
896
|
+
* return HttpResponse.sse(this.llm.complete(id, prompt, { signal: ctx.signal }))
|
|
897
|
+
* }
|
|
898
|
+
* ```
|
|
899
|
+
*/
|
|
900
|
+
static sse(source: SseSource, options?: SseOptions): HttpResponse<SseBody>;
|
|
901
|
+
/** A copy of this response with `name` set to `value`. */
|
|
902
|
+
withHeader(name: string, value: HeaderValue): HttpResponse<T>;
|
|
903
|
+
/** A copy of this response with another status. */
|
|
904
|
+
withStatus(status: number): HttpResponse<T>;
|
|
905
|
+
/**
|
|
906
|
+
* A copy of this response setting a cookie, with the secure defaults (`Path=/; Secure; HttpOnly;
|
|
907
|
+
* SameSite=Lax`, see the plugin option `cookies.defaults`). It replaces a cookie with the same name,
|
|
908
|
+
* domain and path set by `ctx.setCookie()` or an earlier `withCookie()`.
|
|
909
|
+
*
|
|
910
|
+
* ```ts
|
|
911
|
+
* return HttpResponse.ok({ id: user.id }).withCookie('__Host-uid', user.id, { signed: true, maxAge: 8 * 3600 })
|
|
912
|
+
* ```
|
|
913
|
+
*
|
|
914
|
+
* The name, the value, the lifetime, the domain, the path and the size are checked now; the rules
|
|
915
|
+
* depending on the plugin (`secure` default with a prefix or `SameSite=None`, signing, the final
|
|
916
|
+
* size) when the response is sent: a violation then answers a generic 500, logged as
|
|
917
|
+
* `Invalid cookie "name": …`.
|
|
918
|
+
*
|
|
919
|
+
* @throws TypeError / RangeError for an invalid cookie.
|
|
920
|
+
*/
|
|
921
|
+
withCookie(name: string, value: string, options?: CookieOptions): HttpResponse<T>;
|
|
922
|
+
/** A copy of this response clearing a cookie (see `ctx.clearCookie()`). */
|
|
923
|
+
withoutCookie(name: string, options?: ClearCookieOptions): HttpResponse<T>;
|
|
924
|
+
}
|
|
925
|
+
/** Whether `value` is an {@link HttpResponse} (also across duplicated package copies). */
|
|
926
|
+
declare function isHttpResponse(value: unknown): value is HttpResponse;
|
|
927
|
+
/**
|
|
928
|
+
* A guard decides whether a request may reach the handler. Guards run in order: global (plugin
|
|
929
|
+
* options) → controller (`@UseGuard` on the class) → route. Returning `false` responds
|
|
930
|
+
* `403 Forbidden`; throw an `HttpException` (e.g. `UnauthorizedException`) for another response.
|
|
931
|
+
*
|
|
932
|
+
* ```ts
|
|
933
|
+
* @Injectable()
|
|
934
|
+
* export class RolesGuard implements CanActivate {
|
|
935
|
+
* canActivate(ctx: HttpContext): boolean {
|
|
936
|
+
* const roles = Roles.get(ctx) // route metadata from createRouteDecorator
|
|
937
|
+
* return roles === undefined || roles.some(role => currentUser(ctx).roles.includes(role))
|
|
938
|
+
* }
|
|
939
|
+
* }
|
|
940
|
+
* ```
|
|
941
|
+
*/
|
|
942
|
+
interface CanActivate {
|
|
943
|
+
canActivate(ctx: HttpContext): boolean | Promise<boolean>;
|
|
944
|
+
}
|
|
945
|
+
/** Runs the rest of the interceptor chain and the handler; resolves with the handler result. */
|
|
946
|
+
type CallHandler = () => Promise<unknown>;
|
|
947
|
+
/**
|
|
948
|
+
* Wraps the handler (after the guards). Interceptors run global → controller → route, the first one
|
|
949
|
+
* being the outermost. `next()` resolves with the handler result (before serialisation): return it,
|
|
950
|
+
* transform it, or return something else without calling `next()` (cache hit...).
|
|
951
|
+
*
|
|
952
|
+
* ```ts
|
|
953
|
+
* @Injectable()
|
|
954
|
+
* export class TimingInterceptor implements HttpInterceptor {
|
|
955
|
+
* async intercept(ctx: HttpContext, next: CallHandler) {
|
|
956
|
+
* const started = performance.now()
|
|
957
|
+
* try {
|
|
958
|
+
* return await next()
|
|
959
|
+
* } finally {
|
|
960
|
+
* ctx.setHeader('server-timing', `handler;dur=${performance.now() - started}`)
|
|
961
|
+
* }
|
|
962
|
+
* }
|
|
963
|
+
* }
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
interface HttpInterceptor {
|
|
967
|
+
intercept(ctx: HttpContext, next: CallHandler): unknown;
|
|
968
|
+
}
|
|
969
|
+
/** What an {@link ExceptionMapper} may return: `undefined` lets the next mapper (or the default) handle the error. */
|
|
970
|
+
type ExceptionMapperResult = HttpException | HttpResponse | Response | undefined;
|
|
971
|
+
/**
|
|
972
|
+
* Converts errors to responses. Mappers (plugin option `exceptionMappers`) are tried in order for
|
|
973
|
+
* every error of the pipeline, including `404`/`405`; the first one returning something wins. Return
|
|
974
|
+
* an `HttpException` to reuse the default error body, an `HttpResponse` or a Web `Response` for full
|
|
975
|
+
* control, `undefined` to pass.
|
|
976
|
+
*
|
|
977
|
+
* ```ts
|
|
978
|
+
* @Injectable()
|
|
979
|
+
* export class DomainErrorMapper implements ExceptionMapper {
|
|
980
|
+
* map(error: unknown) {
|
|
981
|
+
* if (error instanceof UserNotFoundError) return new NotFoundException(error.message)
|
|
982
|
+
* return undefined
|
|
983
|
+
* }
|
|
984
|
+
* }
|
|
985
|
+
* ```
|
|
986
|
+
*/
|
|
987
|
+
interface ExceptionMapper {
|
|
988
|
+
map(error: unknown, ctx: HttpContext): ExceptionMapperResult | Promise<ExceptionMapperResult>;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Static side of a guard class: the route decorators whose decisions the guard enforces. Read by
|
|
992
|
+
* `HttpPlugin` at boot: a route whose decision belongs to a required group (or to a group some
|
|
993
|
+
* guard enforces) must have a guard enforcing that group in its chain (ZNIGHT023 otherwise).
|
|
994
|
+
*
|
|
995
|
+
* ```ts
|
|
996
|
+
* @Injectable()
|
|
997
|
+
* export class AuthGuard implements CanActivate {
|
|
998
|
+
* static readonly enforces = [Public, Authenticated, Roles]
|
|
999
|
+
* canActivate(ctx: HttpContext) { ... }
|
|
1000
|
+
* }
|
|
1001
|
+
* ```
|
|
1002
|
+
*
|
|
1003
|
+
* Typed with {@link RouteDecoratorInfo} (not `RouteDecorator<unknown>`) so that `[Public, Roles]`
|
|
1004
|
+
* stays assignable under `strictFunctionTypes`.
|
|
1005
|
+
*/
|
|
1006
|
+
interface GuardEnforcement {
|
|
1007
|
+
readonly enforces?: readonly RouteDecoratorInfo[];
|
|
1008
|
+
}
|
|
1009
|
+
/** A class producing {@link CanActivate} instances (for `@UseGuard` and plugin options). */
|
|
1010
|
+
type GuardClass = (abstract new (...args: never[]) => CanActivate) & GuardEnforcement;
|
|
1011
|
+
/** A class producing {@link HttpInterceptor} instances. */
|
|
1012
|
+
type HttpInterceptorClass = abstract new (...args: never[]) => HttpInterceptor;
|
|
1013
|
+
/** A class producing {@link ExceptionMapper} instances. */
|
|
1014
|
+
type ExceptionMapperClass = abstract new (...args: never[]) => ExceptionMapper;
|
|
1015
|
+
/** Options of {@link Controller} (static). */
|
|
1016
|
+
interface ControllerOptions {
|
|
1017
|
+
/** Base path of every route of the controller (`/users`). Default: `/`. */
|
|
1018
|
+
readonly path?: string;
|
|
1019
|
+
/** Provider scope. Default: `singleton`; `request` creates one controller per request. */
|
|
1020
|
+
readonly scope?: Scope;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Declares a controller: a provider (singleton by default) whose decorated methods are routes.
|
|
1024
|
+
*
|
|
1025
|
+
* ```ts
|
|
1026
|
+
* @Controller('/users')
|
|
1027
|
+
* export class UserController {
|
|
1028
|
+
* constructor(private readonly users: UserService) {}
|
|
1029
|
+
*
|
|
1030
|
+
* @Get('/:id')
|
|
1031
|
+
* find(@Param('id') id: number) {
|
|
1032
|
+
* return this.users.findById(id)
|
|
1033
|
+
* }
|
|
1034
|
+
* }
|
|
1035
|
+
* ```
|
|
1036
|
+
*
|
|
1037
|
+
* @param pathOrOptions static: base path, or `{ path?, scope? }`.
|
|
1038
|
+
*/
|
|
1039
|
+
declare function Controller(_pathOrOptions?: string | ControllerOptions): ClassMarker;
|
|
1040
|
+
/** `GET` route. `path` (static) is relative to the controller path; default: the controller path. */
|
|
1041
|
+
declare function Get(_path?: string): MethodMarker;
|
|
1042
|
+
/** `POST` route (default status 201). */
|
|
1043
|
+
declare function Post(_path?: string): MethodMarker;
|
|
1044
|
+
/** `PUT` route. */
|
|
1045
|
+
declare function Put(_path?: string): MethodMarker;
|
|
1046
|
+
/** `PATCH` route. */
|
|
1047
|
+
declare function Patch(_path?: string): MethodMarker;
|
|
1048
|
+
/** `DELETE` route. */
|
|
1049
|
+
declare function Delete(_path?: string): MethodMarker;
|
|
1050
|
+
/** `HEAD` route (`GET` routes already answer `HEAD` requests). */
|
|
1051
|
+
declare function Head(_path?: string): MethodMarker;
|
|
1052
|
+
/** `OPTIONS` route (paths without one answer `OPTIONS` with `204` and an `allow` header). */
|
|
1053
|
+
declare function Options(_path?: string): MethodMarker;
|
|
1054
|
+
/** Route for GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS (methods without a dedicated route on the same path). */
|
|
1055
|
+
declare function All(_path?: string): MethodMarker;
|
|
1056
|
+
/**
|
|
1057
|
+
* A `GET` route serving an event stream (Server-Sent Events). The handler returns an
|
|
1058
|
+
* `AsyncIterable`, an `Iterable` or a `ReadableStream` of `SseEvent` (`{ data, event, id, retry,
|
|
1059
|
+
* comment }`), or an `HttpResponse` / exception for other outcomes (204 stops `EventSource`). The
|
|
1060
|
+
* plugin frames the events, sends heartbeats, stops the source when the client leaves (`ctx.signal`)
|
|
1061
|
+
* and closes the stream gracefully on shutdown.
|
|
1062
|
+
*
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* @Sse('/:id/events', { retry: 5_000 })
|
|
1065
|
+
* async events(@Param('id') id: string, @Ctx() ctx: HttpContext): Promise<AsyncIterable<SseEvent<OrderEvent>>> {
|
|
1066
|
+
* if ((await this.orders.find(id)) === undefined) throw new NotFoundException() // pre-flight checks first
|
|
1067
|
+
* return this.hub.subscribe(id, { signal: ctx.signal })
|
|
1068
|
+
* }
|
|
1069
|
+
* ```
|
|
1070
|
+
*
|
|
1071
|
+
* Prefer an `async` method returning the source over an `async *` generator: the body of a
|
|
1072
|
+
* generator only runs once the 200 and the headers are sent.
|
|
1073
|
+
*
|
|
1074
|
+
* @param path static: route path (relative to the controller path).
|
|
1075
|
+
* @param options static: `{ heartbeat?, retry?, maxDuration? }` (see `SseOptions`).
|
|
1076
|
+
*/
|
|
1077
|
+
declare function Sse(path?: string, options?: SseOptions): MethodMarker;
|
|
1078
|
+
declare function Sse(options: SseOptions): MethodMarker;
|
|
1079
|
+
/**
|
|
1080
|
+
* Injects a route parameter, coerced to the parameter type (`string`, `number`, `boolean`,
|
|
1081
|
+
* `bigint`, `Date`, literal unions) or validated with `schema`. Without arguments: every parameter
|
|
1082
|
+
* (`Record<string, string>`).
|
|
1083
|
+
*
|
|
1084
|
+
* ```ts
|
|
1085
|
+
* @Get('/:id')
|
|
1086
|
+
* find(@Param('id') id: number) {}
|
|
1087
|
+
* ```
|
|
1088
|
+
*
|
|
1089
|
+
* @param name static: name of a `:param` of the route path.
|
|
1090
|
+
* @param schema value: a Standard Schema validating the raw string (its output is injected).
|
|
1091
|
+
*/
|
|
1092
|
+
declare function Param(): ParameterMarker;
|
|
1093
|
+
declare function Param(name: string, schema?: StandardSchemaV1): ParameterMarker;
|
|
1094
|
+
declare function Param(schema: StandardSchemaV1): ParameterMarker;
|
|
1095
|
+
/**
|
|
1096
|
+
* Injects a query parameter (coerced to the parameter type; `T[]` accepts repeated and single
|
|
1097
|
+
* values; optional when the parameter is optional or has a default) or, without name, the whole
|
|
1098
|
+
* query (`Record<string, string | string[]>`), typically validated with a schema.
|
|
1099
|
+
*
|
|
1100
|
+
* ```ts
|
|
1101
|
+
* list(@Query('page') page = 1, @Query('tag') tags: string[] = []) {}
|
|
1102
|
+
* search(@Query(SearchQuery) query: SearchQuery) {}
|
|
1103
|
+
* ```
|
|
1104
|
+
*
|
|
1105
|
+
* @param name static: query key.
|
|
1106
|
+
* @param schema value: a Standard Schema (its output is injected).
|
|
1107
|
+
*/
|
|
1108
|
+
declare function Query(): ParameterMarker;
|
|
1109
|
+
declare function Query(name: string, schema?: StandardSchemaV1): ParameterMarker;
|
|
1110
|
+
declare function Query(schema: StandardSchemaV1): ParameterMarker;
|
|
1111
|
+
/**
|
|
1112
|
+
* Injects a request cookie (decoded, coerced to the parameter type like `@Headers`: `string`,
|
|
1113
|
+
* `number`, `boolean`, `bigint`, `Date`, literal unions, enums, branded primitives) or validated
|
|
1114
|
+
* with `schema`; without name, every cookie (`Record<string, string>`). An absent cookie answers 400
|
|
1115
|
+
* `Missing cookie "name"` unless the parameter is optional. Error messages never echo the value.
|
|
1116
|
+
*
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* // a stale or forged value falls back instead of answering 400 until the cookie expires
|
|
1119
|
+
* export const Theme = z.enum(['light', 'dark']).catch('light')
|
|
1120
|
+
*
|
|
1121
|
+
* read(@Cookie('theme', Theme) theme: 'light' | 'dark', @Cookie('visits') visits = 0) {}
|
|
1122
|
+
* ```
|
|
1123
|
+
*
|
|
1124
|
+
* @param name static: cookie name (an RFC 9110 token).
|
|
1125
|
+
* @param schema value: a Standard Schema (its output is injected).
|
|
1126
|
+
*/
|
|
1127
|
+
declare function Cookie(): ParameterMarker;
|
|
1128
|
+
declare function Cookie(name: string, schema?: StandardSchemaV1): ParameterMarker;
|
|
1129
|
+
declare function Cookie(schema: StandardSchemaV1): ParameterMarker;
|
|
1130
|
+
/**
|
|
1131
|
+
* Injects the **verified** value of a signed cookie (set with `{ signed: true }`, plugin option
|
|
1132
|
+
* `cookies.secrets`): absent, tampered or signed with a retired key, the cookie does not exist
|
|
1133
|
+
* (400 `Missing cookie "name"` unless the parameter is optional). Same coercion and schema rules as
|
|
1134
|
+
* {@link Cookie}. A route reading a signed cookie fails the boot when `cookies.secrets` is not set.
|
|
1135
|
+
*
|
|
1136
|
+
* @param name static: cookie name.
|
|
1137
|
+
* @param schema value: a Standard Schema (its output is injected).
|
|
1138
|
+
*/
|
|
1139
|
+
declare function SignedCookie(_name: string, _schema?: StandardSchemaV1): ParameterMarker;
|
|
1140
|
+
/**
|
|
1141
|
+
* Injects the parsed request body, validated with `schema` when given (the schema output is
|
|
1142
|
+
* injected: defaults, transforms, stripped keys). A non-primitive body without schema is reported
|
|
1143
|
+
* by the compiler (ZNIGHT017).
|
|
1144
|
+
*
|
|
1145
|
+
* @param schema value: a Standard Schema (Zod, Valibot, ArkType...).
|
|
1146
|
+
*/
|
|
1147
|
+
declare function Body(_schema?: StandardSchemaV1): ParameterMarker;
|
|
1148
|
+
/**
|
|
1149
|
+
* Injects a request header (case-insensitive `name`, coerced to the parameter type) or, without
|
|
1150
|
+
* name, every header (`Record<string, string | undefined>`).
|
|
1151
|
+
*
|
|
1152
|
+
* @param name static: header name.
|
|
1153
|
+
*/
|
|
1154
|
+
declare function Headers(_name?: string): ParameterMarker;
|
|
1155
|
+
/**
|
|
1156
|
+
* Injects a file of a `multipart/form-data` body as a standard `File` (`name`, `type`, `size`,
|
|
1157
|
+
* `arrayBuffer()`, `text()`, `stream()`), held in memory. The route parses multipart bodies before
|
|
1158
|
+
* its interceptors, enforcing the options while the body is read: a file above `maxSize` answers
|
|
1159
|
+
* 413, a refused type 415, a second file in the field or a file in an undeclared field 400, a
|
|
1160
|
+
* missing required file 400, a non-multipart body 415. `@Body()` receives the text fields.
|
|
1161
|
+
*
|
|
1162
|
+
* ```ts
|
|
1163
|
+
* @Post('/avatar')
|
|
1164
|
+
* upload(@UploadedFile('avatar', { maxSize: '2mb', mimeTypes: ['image/png', 'image/jpeg'] }) avatar: File) {}
|
|
1165
|
+
*
|
|
1166
|
+
* @Post('/attachment')
|
|
1167
|
+
* attach(@UploadedFile('file') file?: File) {} // optional: undefined when absent
|
|
1168
|
+
* ```
|
|
1169
|
+
*
|
|
1170
|
+
* `File.name` and `File.type` are sent by the client: never trust them (see the README, "Files").
|
|
1171
|
+
*
|
|
1172
|
+
* @param field static: the multipart field; omitted: the single file of the body, whatever its field.
|
|
1173
|
+
* @param options static: `{ required?, maxSize?, mimeTypes? }`.
|
|
1174
|
+
*/
|
|
1175
|
+
declare function UploadedFile(field?: string, options?: UploadedFileOptions): ParameterMarker;
|
|
1176
|
+
declare function UploadedFile(options: UploadedFileOptions): ParameterMarker;
|
|
1177
|
+
/**
|
|
1178
|
+
* Injects the files of a multipart field (`File[]`, in body order), or of every field without
|
|
1179
|
+
* `field`. Same rules as {@link UploadedFile}; above `maxFiles` files the request answers 413. An
|
|
1180
|
+
* optional parameter (`files?: File[]`, `required: false`) receives an empty array when no file is
|
|
1181
|
+
* sent.
|
|
1182
|
+
*
|
|
1183
|
+
* ```ts
|
|
1184
|
+
* @Post('/documents')
|
|
1185
|
+
* add(@UploadedFiles('documents', { maxFiles: 5, maxSize: '20mb', mimeTypes: ['application/pdf'] }) documents: File[]) {}
|
|
1186
|
+
* ```
|
|
1187
|
+
*
|
|
1188
|
+
* @param field static: the multipart field; omitted: the files of every field.
|
|
1189
|
+
* @param options static: `{ required?, maxSize?, maxFiles?, mimeTypes? }`.
|
|
1190
|
+
*/
|
|
1191
|
+
declare function UploadedFiles(field?: string, options?: UploadedFilesOptions): ParameterMarker;
|
|
1192
|
+
declare function UploadedFiles(options: UploadedFilesOptions): ParameterMarker;
|
|
1193
|
+
/**
|
|
1194
|
+
* Sets the maximum request body size of a route, or of every route of a controller (the route wins),
|
|
1195
|
+
* over the plugin `bodyLimit` (and over the limit computed for upload routes). `Infinity` disables
|
|
1196
|
+
* the limit, e.g. for a route streaming its body with `ctx.bodyStream()`.
|
|
1197
|
+
*
|
|
1198
|
+
* ```ts
|
|
1199
|
+
* @Put('/files/:name')
|
|
1200
|
+
* @BodyLimit('5gb')
|
|
1201
|
+
* async store(@Param('name') name: string, @Ctx() ctx: HttpContext) {
|
|
1202
|
+
* await pipeline(Readable.fromWeb(ctx.bodyStream()), createWriteStream(join(DIR, safeName(name))))
|
|
1203
|
+
* }
|
|
1204
|
+
* ```
|
|
1205
|
+
*
|
|
1206
|
+
* @param size static: bytes (`10_485_760`) or a size with a unit (`'10mb'`, `'512kb'`; `kb` = 1024 bytes).
|
|
1207
|
+
*/
|
|
1208
|
+
declare function BodyLimit(_size: ByteSize): ClassMarker & MethodMarker;
|
|
1209
|
+
/** Injects the native request of the adapter (escape hatch; the compiler warns). */
|
|
1210
|
+
declare function Req(): ParameterMarker;
|
|
1211
|
+
/**
|
|
1212
|
+
* Injects the native response of the adapter (escape hatch; the compiler warns). The handler then
|
|
1213
|
+
* writes the response itself, and must have finished when its promise settles.
|
|
1214
|
+
*/
|
|
1215
|
+
declare function Res(): ParameterMarker;
|
|
1216
|
+
/** Injects the {@link HttpContext} of the request. */
|
|
1217
|
+
declare function Ctx(): ParameterMarker;
|
|
1218
|
+
/**
|
|
1219
|
+
* Sets the status of successful responses (wins over `HttpResponse.status` and the defaults).
|
|
1220
|
+
*
|
|
1221
|
+
* @param status static: an integer between 100 and 599.
|
|
1222
|
+
*/
|
|
1223
|
+
declare function HttpCode(_status: number): MethodMarker;
|
|
1224
|
+
/**
|
|
1225
|
+
* Adds a header to successful responses. Repeat the decorator for several headers.
|
|
1226
|
+
*
|
|
1227
|
+
* @param name static: header name.
|
|
1228
|
+
* @param value static: header value.
|
|
1229
|
+
*/
|
|
1230
|
+
declare function Header(_name: string, _value: string): MethodMarker;
|
|
1231
|
+
/**
|
|
1232
|
+
* Protects a controller (every route) or a route with guards (providers implementing
|
|
1233
|
+
* {@link CanActivate}). Order: global guards → controller guards → route guards, each list in
|
|
1234
|
+
* declaration order.
|
|
1235
|
+
*
|
|
1236
|
+
* @param guards value: guard classes.
|
|
1237
|
+
*/
|
|
1238
|
+
declare function UseGuard(..._guards: [GuardClass, ...GuardClass[]]): ClassMarker & MethodMarker;
|
|
1239
|
+
/**
|
|
1240
|
+
* Wraps a controller (every route) or a route with interceptors (providers implementing
|
|
1241
|
+
* {@link HttpInterceptor}). Order: global → controller → route, the first one being the outermost.
|
|
1242
|
+
*
|
|
1243
|
+
* @param interceptors value: interceptor classes.
|
|
1244
|
+
*/
|
|
1245
|
+
declare function UseInterceptor(..._interceptors: [HttpInterceptorClass, ...HttpInterceptorClass[]]): ClassMarker & MethodMarker;
|
|
1246
|
+
/**
|
|
1247
|
+
* Validates the request body with `schema`: sugar for `@Body(schema)` on the body parameter (using
|
|
1248
|
+
* both is ZNIGHT005).
|
|
1249
|
+
*
|
|
1250
|
+
* @param schema value: a Standard Schema.
|
|
1251
|
+
*/
|
|
1252
|
+
declare function Validate(_schema: StandardSchemaV1): MethodMarker;
|
|
1253
|
+
/** Arguments of a route decorator: optional when `T` accepts `undefined` or is `true` (flag decorators). */
|
|
1254
|
+
type RouteDecoratorArgs<T> = [T] extends [true] ? [value?: T] : undefined extends T ? [value?: T] : [value: T];
|
|
1255
|
+
/** Options of {@link createRouteDecorator} (static). */
|
|
1256
|
+
interface RouteDecoratorOptions {
|
|
1257
|
+
/**
|
|
1258
|
+
* Decision group. Decorators of a group are alternatives: one per controller or route, and the
|
|
1259
|
+
* route's decision replaces the controller's. Default: the key (the decorator is its own group).
|
|
1260
|
+
* A letter followed by letters, digits, `_`, `.` or `-`.
|
|
1261
|
+
*/
|
|
1262
|
+
readonly group?: string;
|
|
1263
|
+
/**
|
|
1264
|
+
* Every route must carry a decision of the group: ZNIGHT023 at build time when a route has none,
|
|
1265
|
+
* and at boot when no guard of its chain enforces it (see {@link GuardClass}). All the decorators
|
|
1266
|
+
* of a group must agree. Default `false`.
|
|
1267
|
+
*/
|
|
1268
|
+
readonly required?: boolean;
|
|
1269
|
+
}
|
|
1270
|
+
/** The data part of a route decorator: what guards list in `static enforces`. */
|
|
1271
|
+
interface RouteDecoratorInfo {
|
|
1272
|
+
/** Key of the value in `route.meta`. */
|
|
1273
|
+
readonly key: string;
|
|
1274
|
+
/** Decision group (`options.group ?? key`). */
|
|
1275
|
+
readonly group: string;
|
|
1276
|
+
/** Every route must carry a decision of the group. */
|
|
1277
|
+
readonly required: boolean;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* A route metadata entry given as data rather than as a decorator: the `meta` of plugin routes
|
|
1281
|
+
* (`[Public.meta()]`). Built by {@link RouteDecorator.meta}.
|
|
1282
|
+
*/
|
|
1283
|
+
interface RouteMetaEntry<T = unknown> {
|
|
1284
|
+
readonly key: string;
|
|
1285
|
+
readonly value: T;
|
|
1286
|
+
/** Decision group. Default: the key. */
|
|
1287
|
+
readonly group?: string;
|
|
1288
|
+
/** Default `false`. */
|
|
1289
|
+
readonly required?: boolean;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Anything that carries effective route metadata: the {@link HttpContext} of a request, and the
|
|
1293
|
+
* contexts of other transports that reuse route decorators (a GraphQL resolver context, a WebSocket
|
|
1294
|
+
* gateway context). {@link RouteDecorator.get} reads `route.meta[key]`; nothing else is required.
|
|
1295
|
+
*
|
|
1296
|
+
* ```ts
|
|
1297
|
+
* const carrier: RouteMetaCarrier = { route: { meta: { roles: ['admin'] } } }
|
|
1298
|
+
* Roles.get(carrier) // ['admin']
|
|
1299
|
+
* ```
|
|
1300
|
+
*/
|
|
1301
|
+
interface RouteMetaCarrier {
|
|
1302
|
+
/** The matched route (or field, or handler), `undefined` when there is none. */
|
|
1303
|
+
readonly route?: {
|
|
1304
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
1305
|
+
} | undefined;
|
|
1306
|
+
}
|
|
1307
|
+
/** A user-defined route metadata decorator created with {@link createRouteDecorator}. */
|
|
1308
|
+
interface RouteDecorator<T> extends RouteDecoratorInfo {
|
|
1309
|
+
(...args: RouteDecoratorArgs<T>): ClassMarker & MethodMarker;
|
|
1310
|
+
/**
|
|
1311
|
+
* The value of the current route, if set. Reads the effective metadata: the route's decision of
|
|
1312
|
+
* a group wins over the controller's. Accepts any {@link RouteMetaCarrier}: an
|
|
1313
|
+
* {@link HttpContext}, or the context of another transport exposing `route.meta`.
|
|
1314
|
+
*/
|
|
1315
|
+
get(ctx: RouteMetaCarrier): T | undefined;
|
|
1316
|
+
/**
|
|
1317
|
+
* The same metadata as data (`{ key, value, group, required }`), for routes that are not compiled
|
|
1318
|
+
* controllers (plugin routes: `OpenApiPlugin.configure({ meta: [Public.meta()] })`). The value is
|
|
1319
|
+
* `true` when omitted, like `@Public()`.
|
|
1320
|
+
*/
|
|
1321
|
+
meta(...args: RouteDecoratorArgs<T>): RouteMetaEntry<T>;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Creates a metadata decorator for controllers and routes. The compiler stores its argument
|
|
1325
|
+
* (`value`, statically evaluated or an exported reference; `true` when omitted) in
|
|
1326
|
+
* `meta[key]`, read by guards and interceptors through `ctx.route.meta` or `decorator.get(ctx)`.
|
|
1327
|
+
*
|
|
1328
|
+
* ```ts
|
|
1329
|
+
* export const Public = createRouteDecorator('public', { group: 'access', required: true })
|
|
1330
|
+
* export const Authenticated = createRouteDecorator('authenticated', { group: 'access', required: true })
|
|
1331
|
+
* export const Roles = createRouteDecorator<readonly string[]>('roles', { group: 'access', required: true })
|
|
1332
|
+
*
|
|
1333
|
+
* @Controller('/books')
|
|
1334
|
+
* @Roles(['reader']) // the decision of every route of the controller...
|
|
1335
|
+
* export class BookController {
|
|
1336
|
+
* @Get('/:id/cover')
|
|
1337
|
+
* @Public() // ...unless the route has its own decision of the group
|
|
1338
|
+
* cover() {}
|
|
1339
|
+
* }
|
|
1340
|
+
*
|
|
1341
|
+
* // in a guard
|
|
1342
|
+
* if (Public.get(ctx)) return true
|
|
1343
|
+
* const roles = Roles.get(ctx) ?? []
|
|
1344
|
+
* ```
|
|
1345
|
+
*
|
|
1346
|
+
* Decorators of one `group` are alternatives: two of them on the same class or method are
|
|
1347
|
+
* ZNIGHT005, and the route's decision replaces the controller's (without `group`, each decorator is
|
|
1348
|
+
* its own group: the route value wins over the controller value). With `required: true`, a route
|
|
1349
|
+
* without decision fails the build (ZNIGHT023).
|
|
1350
|
+
*
|
|
1351
|
+
* @param key static: unique metadata key.
|
|
1352
|
+
* @param options static: `{ group?, required? }`.
|
|
1353
|
+
*/
|
|
1354
|
+
declare function createRouteDecorator<T = true>(key: string, options?: RouteDecoratorOptions): RouteDecorator<T>;
|
|
1355
|
+
/**
|
|
1356
|
+
* Origins allowed to read responses: `'*'` (any origin), an exact origin
|
|
1357
|
+
* (`'https://app.example.com'`), an anchored `RegExp` (`/^https:\/\/[a-z0-9-]+\.example\.com$/`), a
|
|
1358
|
+
* list of those, or a function (awaited; a function that throws or rejects denies the origin).
|
|
1359
|
+
*/
|
|
1360
|
+
type CorsOrigin = "*" | string | RegExp | readonly (string | RegExp)[] | ((origin: string) => boolean | Promise<boolean>);
|
|
1361
|
+
/** The `cors` option of `HttpPlugin.configure`. */
|
|
1362
|
+
interface CorsOptions {
|
|
1363
|
+
/** Allowed origins (required). */
|
|
1364
|
+
readonly origin: CorsOrigin;
|
|
1365
|
+
/** `access-control-allow-credentials: true` (cookies, `Authorization`). Not with `'*'`. Default `false`. */
|
|
1366
|
+
readonly credentials?: boolean;
|
|
1367
|
+
/**
|
|
1368
|
+
* `access-control-allow-methods` of preflights. Default: the methods routed on the requested path.
|
|
1369
|
+
* `'*'` (any method) is refused with `credentials`, where browsers read it as a method named `*`.
|
|
1370
|
+
*/
|
|
1371
|
+
readonly methods?: readonly string[];
|
|
1372
|
+
/**
|
|
1373
|
+
* `access-control-allow-headers` of preflights. Default: the headers the browser asks for
|
|
1374
|
+
* (`access-control-request-headers`). `'*'` is refused with `credentials`; it covers
|
|
1375
|
+
* `Authorization` too (sent as `*, authorization` when the browser asks for it: browsers never let
|
|
1376
|
+
* `*` alone cover it).
|
|
1377
|
+
*/
|
|
1378
|
+
readonly allowHeaders?: readonly string[] | "*";
|
|
1379
|
+
/**
|
|
1380
|
+
* `access-control-expose-headers` of replies. Default: the request id header (none when it is
|
|
1381
|
+
* `false`). `'*'` (every header) is refused with `credentials`, where browsers read it literally.
|
|
1382
|
+
*/
|
|
1383
|
+
readonly exposeHeaders?: readonly string[];
|
|
1384
|
+
/** `access-control-max-age` of preflights, in seconds (0 to 86400). Default `600`. */
|
|
1385
|
+
readonly maxAge?: number;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Client side of Server-Sent Events: the WHATWG "interpreting an event stream" algorithm over a
|
|
1389
|
+
* byte stream (`parseSseStream`), used by `injectSse()` and by tests over real sockets. Import-free.
|
|
1390
|
+
*/
|
|
1391
|
+
/** A dispatched event. */
|
|
1392
|
+
interface SseMessage {
|
|
1393
|
+
/** Event type: `message` by default. */
|
|
1394
|
+
readonly event: string;
|
|
1395
|
+
readonly data: string;
|
|
1396
|
+
/** The last event id at dispatch time (what `EventSource` sends back as `Last-Event-ID`). */
|
|
1397
|
+
readonly id: string;
|
|
1398
|
+
/** `JSON.parse(data)`. */
|
|
1399
|
+
json<T = unknown>(): T;
|
|
1400
|
+
}
|
|
1401
|
+
/** An item of an event stream: an event, a comment (heartbeats are `''`) or a reconnection delay. */
|
|
1402
|
+
type SseItem = ({
|
|
1403
|
+
readonly type: "event";
|
|
1404
|
+
} & SseMessage) | {
|
|
1405
|
+
readonly type: "comment";
|
|
1406
|
+
readonly text: string;
|
|
1407
|
+
} | {
|
|
1408
|
+
readonly type: "retry";
|
|
1409
|
+
readonly retry: number;
|
|
1410
|
+
};
|
|
1411
|
+
/** Options of {@link parseSseStream}. */
|
|
1412
|
+
interface ParseSseOptions {
|
|
1413
|
+
/** Stops reading (the body is cancelled) when aborted. */
|
|
1414
|
+
readonly signal?: AbortSignal;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Turns an event stream body (a `fetch()` response body, `injectStream().body`) into its items.
|
|
1418
|
+
* UTF-8 is decoded with the BOM stripped; an unterminated event at the end is discarded. Calling
|
|
1419
|
+
* `return()` (leaving a `for await` loop) or aborting `signal` cancels the body.
|
|
1420
|
+
*
|
|
1421
|
+
* ```ts
|
|
1422
|
+
* const response = await fetch(`${http.address}/orders/o-1/events`)
|
|
1423
|
+
* for await (const item of parseSseStream(response.body!)) {
|
|
1424
|
+
* if (item.type === 'event') console.log(item.event, item.json())
|
|
1425
|
+
* }
|
|
1426
|
+
* ```
|
|
1427
|
+
*/
|
|
1428
|
+
declare function parseSseStream(body: ReadableStream<Uint8Array>, options?: ParseSseOptions): AsyncIterableIterator<SseItem>;
|
|
1429
|
+
/** Options of {@link InjectSseResponse.next} and {@link InjectSseResponse.take}. */
|
|
1430
|
+
interface InjectSseWaitOptions {
|
|
1431
|
+
/** Maximum wait for each event (ms). Default: 2 000. */
|
|
1432
|
+
readonly timeoutMs?: number;
|
|
1433
|
+
}
|
|
1434
|
+
/** The response of `injectSse()`: an event stream read in memory, like `EventSource` would. */
|
|
1435
|
+
interface InjectSseResponse extends AsyncIterable<SseMessage>, AsyncDisposable {
|
|
1436
|
+
readonly status: number;
|
|
1437
|
+
readonly headers: Record<string, string | string[]>;
|
|
1438
|
+
readonly cookies: Readonly<Record<string, SetCookieInfo>>;
|
|
1439
|
+
/** `''` for an event stream; otherwise the whole body, read to its end (an error, a 204...). */
|
|
1440
|
+
readonly body: string;
|
|
1441
|
+
/** `JSON.parse(body)`. */
|
|
1442
|
+
json<T = unknown>(): T;
|
|
1443
|
+
/** The next event (comments skipped). Rejects on timeout (default 2 000 ms) or when the stream ended. */
|
|
1444
|
+
next(options?: InjectSseWaitOptions): Promise<SseMessage>;
|
|
1445
|
+
/** The next `count` events (the timeout applies to each). */
|
|
1446
|
+
take(count: number, options?: InjectSseWaitOptions): Promise<SseMessage[]>;
|
|
1447
|
+
/** Comments received so far (heartbeats are `''`). */
|
|
1448
|
+
readonly comments: readonly string[];
|
|
1449
|
+
/** What a browser would send as `Last-Event-ID` on reconnection. */
|
|
1450
|
+
readonly lastEventId: string;
|
|
1451
|
+
/** The last `retry` received (ms). */
|
|
1452
|
+
readonly retry: number | undefined;
|
|
1453
|
+
/** Resolves when the server ends the stream (reading the remaining events, which stay available to `next()`). */
|
|
1454
|
+
readonly done: Promise<void>;
|
|
1455
|
+
/** Disconnects (cancels the body): the server sees a client disconnect. Idempotent. */
|
|
1456
|
+
close(): Promise<void>;
|
|
1457
|
+
/** Reconnects like `EventSource`: closes this stream and sends the same request plus `last-event-id`. */
|
|
1458
|
+
reconnect(): Promise<InjectSseResponse>;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* The `trustProxy` option: `false` (default: the headers are ignored), a number of trusted hops
|
|
1462
|
+
* (`1`: one load balancer in front of the server), or the addresses of the trusted proxies (IPv4 /
|
|
1463
|
+
* IPv6 addresses, CIDR ranges and the aliases `'loopback'`, `'private'`, `'linklocal'`).
|
|
1464
|
+
*/
|
|
1465
|
+
type TrustProxy = false | number | readonly string[];
|
|
1466
|
+
/** HSTS settings (`strict-transport-security`). */
|
|
1467
|
+
interface HstsOptions {
|
|
1468
|
+
/** `max-age`, in seconds. Default `31536000` (one year). */
|
|
1469
|
+
readonly maxAge?: number;
|
|
1470
|
+
/** `includeSubDomains`. Default `true`. */
|
|
1471
|
+
readonly includeSubDomains?: boolean;
|
|
1472
|
+
/** `preload` (needs `includeSubDomains` and a `maxAge` of at least one year). Default `false`. */
|
|
1473
|
+
readonly preload?: boolean;
|
|
1474
|
+
}
|
|
1475
|
+
/** The `securityHeaders` option of `HttpPlugin.configure` (merged over the defaults). */
|
|
1476
|
+
interface SecurityHeadersOptions {
|
|
1477
|
+
/** `content-security-policy`. Default `"default-src 'none'; frame-ancestors 'none'"`. */
|
|
1478
|
+
readonly contentSecurityPolicy?: string | false;
|
|
1479
|
+
/** `x-frame-options`. Default `'DENY'`. */
|
|
1480
|
+
readonly frameOptions?: "DENY" | "SAMEORIGIN" | false;
|
|
1481
|
+
/** `x-content-type-options: nosniff`. Default `true`. */
|
|
1482
|
+
readonly noSniff?: boolean;
|
|
1483
|
+
/** `referrer-policy` (a policy or a comma-separated fallback list). Default `'no-referrer'`. */
|
|
1484
|
+
readonly referrerPolicy?: string | false;
|
|
1485
|
+
/** `strict-transport-security`. Default `false`; `true`: `max-age=31536000; includeSubDomains`. */
|
|
1486
|
+
readonly hsts?: boolean | HstsOptions;
|
|
1487
|
+
/** `cross-origin-resource-policy`. Default `false`. */
|
|
1488
|
+
readonly crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin" | false;
|
|
1489
|
+
/** `cache-control` of replies that do not set one (`'no-store'` for personal data). Default `false`. */
|
|
1490
|
+
readonly cacheControl?: string | false;
|
|
1491
|
+
}
|
|
1492
|
+
/** How the boot check reports problems. */
|
|
1493
|
+
type EnforcementMode = "error" | "warn";
|
|
1494
|
+
/** How requests whose path ends with an extra `/` are handled. */
|
|
1495
|
+
type TrailingSlashPolicy = "strict" | "ignore" | "redirect";
|
|
1496
|
+
/** A global guard: a provider class (resolved with the container) or an instance. */
|
|
1497
|
+
type GuardRef = GuardClass | CanActivate;
|
|
1498
|
+
/** A global interceptor: a provider class or an instance. */
|
|
1499
|
+
type InterceptorRef = HttpInterceptorClass | HttpInterceptor;
|
|
1500
|
+
/** An exception mapper: a provider class or an instance. */
|
|
1501
|
+
type ExceptionMapperRef = ExceptionMapperClass | ExceptionMapper;
|
|
1502
|
+
/** Options of {@link HttpPlugin.configure}. */
|
|
1503
|
+
interface HttpPluginOptions {
|
|
1504
|
+
/** Server implementation. Default: `nodeAdapter()` (built-in, `node:http`). */
|
|
1505
|
+
readonly adapter?: HttpAdapter;
|
|
1506
|
+
/** Port to listen on (`0` picks a free port). Default: `3000`. */
|
|
1507
|
+
readonly port?: number;
|
|
1508
|
+
/** Interface to bind. Default: `0.0.0.0`. */
|
|
1509
|
+
readonly host?: string;
|
|
1510
|
+
/** Path prefix of every route (`/api`). Default: none. */
|
|
1511
|
+
readonly prefix?: string;
|
|
1512
|
+
/** Guards run before the controller and route guards of every route. */
|
|
1513
|
+
readonly guards?: readonly GuardRef[];
|
|
1514
|
+
/** Interceptors wrapping every route (outermost first). */
|
|
1515
|
+
readonly interceptors?: readonly InterceptorRef[];
|
|
1516
|
+
/** Error-to-response mappers, tried in order (see `ExceptionMapper`). */
|
|
1517
|
+
readonly exceptionMappers?: readonly ExceptionMapperRef[];
|
|
1518
|
+
/**
|
|
1519
|
+
* `strict` (default): `/users/` does not match `/users`. `ignore`: it does. `redirect`: it is
|
|
1520
|
+
* redirected (308) to `/users`.
|
|
1521
|
+
*/
|
|
1522
|
+
readonly trailingSlash?: TrailingSlashPolicy;
|
|
1523
|
+
/** Send the message of unexpected errors in 500 responses (development only). Default: `false`. */
|
|
1524
|
+
readonly exposeErrors?: boolean;
|
|
1525
|
+
/**
|
|
1526
|
+
* Maximum request body size (413 above): bytes or a size with a unit (`'10mb'`; `kb` = 1024
|
|
1527
|
+
* bytes), `Infinity` for no limit. Routes override it with `@BodyLimit`; upload routes default to
|
|
1528
|
+
* the sum of their file limits plus this value. Default: 1 MiB.
|
|
1529
|
+
*/
|
|
1530
|
+
readonly bodyLimit?: ByteSize;
|
|
1531
|
+
/** Limits of multipart bodies and defaults of the upload parameters (see `UploadLimits`). */
|
|
1532
|
+
readonly uploads?: UploadLimits;
|
|
1533
|
+
/**
|
|
1534
|
+
* Header carrying the request id: reused when the client sends a valid one (≤ 128 characters of
|
|
1535
|
+
* `[A-Za-z0-9_-.:@/+=]`), echoed on every response. `false`: always generate, never echo.
|
|
1536
|
+
* Default: `x-request-id`.
|
|
1537
|
+
*/
|
|
1538
|
+
readonly requestIdHeader?: string | false;
|
|
1539
|
+
/** Request id generator. Default: `crypto.randomUUID()`. */
|
|
1540
|
+
readonly generateRequestId?: () => string;
|
|
1541
|
+
/**
|
|
1542
|
+
* Maximum time `onStop` waits for in-flight requests before closing the remaining connections.
|
|
1543
|
+
* Keep it below the application `shutdown.hookTimeoutMs`. `Infinity`: no limit of its own (the
|
|
1544
|
+
* application's `hookTimeoutMs` still bounds `onStop`). Default: 4 000 ms.
|
|
1545
|
+
*/
|
|
1546
|
+
readonly drainTimeoutMs?: number;
|
|
1547
|
+
/**
|
|
1548
|
+
* CORS: preflights are answered before routing and guards (never by a route), and replies to
|
|
1549
|
+
* allowed origins get `access-control-allow-origin`, identically on every adapter. Default: off.
|
|
1550
|
+
*
|
|
1551
|
+
* ```ts
|
|
1552
|
+
* cors: { origin: ['https://app.example.com', /^https:\/\/[a-z0-9-]+\.preview\.example\.com$/], credentials: true }
|
|
1553
|
+
* ```
|
|
1554
|
+
*/
|
|
1555
|
+
readonly cors?: CorsOptions | false;
|
|
1556
|
+
/**
|
|
1557
|
+
* Security headers added to every reply that does not set them (`@Res()` raw replies excepted):
|
|
1558
|
+
* `true` (default) for the API defaults, an object merged over them, `false` for none.
|
|
1559
|
+
*/
|
|
1560
|
+
readonly securityHeaders?: boolean | SecurityHeadersOptions;
|
|
1561
|
+
/**
|
|
1562
|
+
* Proxies trusted for `x-forwarded-for` / `x-forwarded-proto` (`ctx.ip`, `ctx.protocol`): a number
|
|
1563
|
+
* of hops, or addresses, CIDR ranges and the aliases `'loopback'`, `'private'`, `'linklocal'`.
|
|
1564
|
+
* Default `false`: the headers are ignored and `ctx.ip` is the adapter's address.
|
|
1565
|
+
*/
|
|
1566
|
+
readonly trustProxy?: TrustProxy;
|
|
1567
|
+
/**
|
|
1568
|
+
* What the boot check of route decisions does when a route misses a decision of a required group,
|
|
1569
|
+
* or when no guard of its chain enforces its decision (ZNIGHT023): `'error'` (default) fails the
|
|
1570
|
+
* boot, `'warn'` logs one warning per group (tests that deliberately run without guards).
|
|
1571
|
+
*/
|
|
1572
|
+
readonly enforcement?: EnforcementMode;
|
|
1573
|
+
/** Cookies: signing keys and defaults (see {@link HttpCookieOptions}). */
|
|
1574
|
+
readonly cookies?: HttpCookieOptions;
|
|
1575
|
+
/** Server-Sent Events defaults (see {@link HttpSseOptions}). */
|
|
1576
|
+
readonly sse?: HttpSseOptions;
|
|
1577
|
+
}
|
|
1578
|
+
/** The plugin option `cookies`. */
|
|
1579
|
+
interface HttpCookieOptions {
|
|
1580
|
+
/**
|
|
1581
|
+
* HMAC keys of signed cookies (`@SignedCookie`, `{ signed: true }`), at least 32 bytes each: the
|
|
1582
|
+
* first signs, all verify (rotation: deploy `[next, current]`, wait for the longest cookie
|
|
1583
|
+
* lifetime, then `[next]`). Default: none (signed cookies unavailable).
|
|
1584
|
+
*/
|
|
1585
|
+
readonly secrets?: string | Uint8Array | readonly (string | Uint8Array)[];
|
|
1586
|
+
/**
|
|
1587
|
+
* Defaults of every cookie set by the application, merged over the secure defaults
|
|
1588
|
+
* `{ path: '/', secure: true, httpOnly: true, sameSite: 'lax' }` (e.g. `{ secure: false }` for
|
|
1589
|
+
* plain `http://` development setups on a non-localhost host).
|
|
1590
|
+
*/
|
|
1591
|
+
readonly defaults?: CookieDefaults;
|
|
1592
|
+
}
|
|
1593
|
+
/** The plugin option `sse`. */
|
|
1594
|
+
interface HttpSseOptions {
|
|
1595
|
+
/** Default heartbeat (ms, an integer ≥ 1 000, or `false`). Default: 8 000. */
|
|
1596
|
+
readonly heartbeat?: number | false;
|
|
1597
|
+
/** Default `retry` sent first (ms). Default: none. */
|
|
1598
|
+
readonly retry?: number;
|
|
1599
|
+
/**
|
|
1600
|
+
* Time an event source gets to finish after `ctx.signal` aborted on shutdown or `maxDuration` (ms),
|
|
1601
|
+
* before the stream ends with a jittered `retry`. Must be lower than `drainTimeoutMs`. Default:
|
|
1602
|
+
* 1 000.
|
|
1603
|
+
*/
|
|
1604
|
+
readonly shutdownGraceMs?: number;
|
|
1605
|
+
}
|
|
1606
|
+
/** A route registered by the plugin (see {@link HttpPlugin.routes}). */
|
|
1607
|
+
interface HttpRouteSummary {
|
|
1608
|
+
readonly method: string;
|
|
1609
|
+
/** Registered path, prefix included. */
|
|
1610
|
+
readonly path: string;
|
|
1611
|
+
/** Controller display name, or the name of the plugin declaring the route. */
|
|
1612
|
+
readonly controller: string;
|
|
1613
|
+
readonly handler: string;
|
|
1614
|
+
/** `'controller'` for compiled routes, the plugin name for plugin routes. */
|
|
1615
|
+
readonly source: string;
|
|
1616
|
+
/** Effective route metadata (the nearest decision of each group). */
|
|
1617
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
1618
|
+
/** Display names of the guards, in chain order (global, controller, route). */
|
|
1619
|
+
readonly guards: readonly string[];
|
|
1620
|
+
/** `'websocket'` for a WebSocket upgrade route (RFC 0003): a gateway of `@znight/websocket`. */
|
|
1621
|
+
readonly upgrade?: "websocket";
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* The HTTP plugin: serves the controllers compiled into `metadata.extensions.http` with the chosen
|
|
1625
|
+
* adapter.
|
|
1626
|
+
*
|
|
1627
|
+
* ```ts
|
|
1628
|
+
* const app = await Application.create({
|
|
1629
|
+
* metadata: __ZNIGHT_METADATA__,
|
|
1630
|
+
* plugins: [HttpPlugin.configure({ port: 3000, prefix: '/api' })],
|
|
1631
|
+
* })
|
|
1632
|
+
* await app.start()
|
|
1633
|
+
* ```
|
|
1634
|
+
*
|
|
1635
|
+
* Lifecycle: `register` declares the request-scoped externals `HttpContext` and `RequestId`;
|
|
1636
|
+
* `onBootstrap` resolves the options, controllers, guards and interceptors, collects the routes of
|
|
1637
|
+
* other plugins (`httpRoutes()`), checks route decisions and registers the routes; `onStart`
|
|
1638
|
+
* listens; `onStop` stops accepting connections and drains in-flight requests (`drainTimeoutMs`);
|
|
1639
|
+
* `onDispose` closes the server.
|
|
1640
|
+
*/
|
|
1641
|
+
declare class HttpPlugin implements Plugin {
|
|
1642
|
+
#private;
|
|
1643
|
+
readonly name = "http";
|
|
1644
|
+
private constructor();
|
|
1645
|
+
/**
|
|
1646
|
+
* Creates the plugin. `options` may be a function of the container, evaluated in `onBootstrap`:
|
|
1647
|
+
* `HttpPlugin.configure(c => ({ port: c.get(AppConfig).port }))`.
|
|
1648
|
+
*/
|
|
1649
|
+
static configure(options?: PluginOptions<HttpPluginOptions>): HttpPlugin;
|
|
1650
|
+
/** The adapter (available after `onBootstrap`). */
|
|
1651
|
+
get adapter(): HttpAdapter;
|
|
1652
|
+
/** Address of the server once listening (`String(address)` is its URL), `undefined` otherwise. */
|
|
1653
|
+
get address(): HttpServerAddress | undefined;
|
|
1654
|
+
/**
|
|
1655
|
+
* The path prefix of every route (`''` without prefix, `/api`), for plugins that serve routes or
|
|
1656
|
+
* publish URLs. Readable once the plugin is registered (before or after its `onBootstrap`; at any
|
|
1657
|
+
* time when the options are not a function of the container). Throws the options error when
|
|
1658
|
+
* the options are invalid.
|
|
1659
|
+
*/
|
|
1660
|
+
get prefix(): string;
|
|
1661
|
+
/** Registered routes (after `onBootstrap`), in registration order. */
|
|
1662
|
+
get routes(): readonly HttpRouteSummary[];
|
|
1663
|
+
/** Number of requests being processed. */
|
|
1664
|
+
get inflight(): number;
|
|
1665
|
+
/** Number of open event streams (`@Sse`, `HttpResponse.sse()`): health checks, gauges, stream caps. */
|
|
1666
|
+
get streams(): number;
|
|
1667
|
+
/**
|
|
1668
|
+
* The signed wire value of a cookie (`<encoded value>.<signature>`, signed with the first secret of
|
|
1669
|
+
* `cookies.secrets`): what a browser would send back after `setCookie(name, value, { signed: true })`.
|
|
1670
|
+
* For gateways minting cookies and for tests. Available after `onBootstrap`.
|
|
1671
|
+
*
|
|
1672
|
+
* @throws TypeError for an invalid name or value; Error without `cookies.secrets`.
|
|
1673
|
+
*/
|
|
1674
|
+
signCookie(name: string, value: string): string;
|
|
1675
|
+
/**
|
|
1676
|
+
* The provider classes the plugin resolves in `onBootstrap` although no compiled metadata
|
|
1677
|
+
* references them: the classes of the `guards`, `interceptors` and `exceptionMappers` options
|
|
1678
|
+
* (instances are left out), in that order, without duplicates. `TestApp` from `@znight/testing`
|
|
1679
|
+
* creates them at boot with their dependencies (an async dependency cannot be created on demand);
|
|
1680
|
+
* pass them to `init: { only }` when restricting the boot with `Application.create()`.
|
|
1681
|
+
*
|
|
1682
|
+
* Options given as a function of the container cannot be read before the bootstrap: the result is
|
|
1683
|
+
* then empty. Never throws: invalid options are reported by `onBootstrap`.
|
|
1684
|
+
*/
|
|
1685
|
+
initRoots(): readonly ProviderKey[];
|
|
1686
|
+
/**
|
|
1687
|
+
* Sends a request in memory through the adapter and the whole pipeline (no socket; the server
|
|
1688
|
+
* does not need to listen). Available after `onBootstrap`.
|
|
1689
|
+
*
|
|
1690
|
+
* ```ts
|
|
1691
|
+
* const res = await http.inject({ method: 'POST', url: '/users', body: { name: 'Ada' } })
|
|
1692
|
+
* expect(res.status).toBe(201)
|
|
1693
|
+
* expect(res.json()).toEqual({ id: 1, name: 'Ada' })
|
|
1694
|
+
* ```
|
|
1695
|
+
*/
|
|
1696
|
+
inject(request: InjectRequest): Promise<InjectResponse>;
|
|
1697
|
+
/**
|
|
1698
|
+
* Sends a request in memory and resolves at the response headers, the body being read as it is
|
|
1699
|
+
* produced (`body`). Cancelling `body` (or aborting `signal`) disconnects the client. Requires an
|
|
1700
|
+
* adapter implementing `injectStream()` (the four built-in ones do).
|
|
1701
|
+
*/
|
|
1702
|
+
injectStream(request: InjectStreamOptions): Promise<InjectStreamResponse>;
|
|
1703
|
+
/**
|
|
1704
|
+
* Opens an event stream in memory (`accept: text/event-stream`) and reads its events like
|
|
1705
|
+
* `EventSource`: `next()`, `take()`, `comments`, `retry`, `lastEventId`, `reconnect()`, `close()`
|
|
1706
|
+
* (the server sees a client disconnect). A response that is not an event stream (404, 204...) is
|
|
1707
|
+
* read to its end (`body`). Resolves at the first chunk of the stream (an `@Sse` stream sends
|
|
1708
|
+
* `retry` or a comment at once), or after 2 000 ms for a stream that sends nothing yet; rejects,
|
|
1709
|
+
* naming the request (which is aborted), when no response headers arrive within 2 000 ms.
|
|
1710
|
+
*
|
|
1711
|
+
* ```ts
|
|
1712
|
+
* await using stream = await http.injectSse({ url: '/orders/o-1/events' })
|
|
1713
|
+
* const event = await stream.next() // rejects after 2 000 ms
|
|
1714
|
+
* expect(event.json()).toMatchObject({ status: 'paid' })
|
|
1715
|
+
* ```
|
|
1716
|
+
*/
|
|
1717
|
+
injectSse(request: InjectStreamOptions): Promise<InjectSseResponse>;
|
|
1718
|
+
/**
|
|
1719
|
+
* Runs a WebSocket handshake in memory (RFC 0003, `WebSocketPlugin.connect()`): the same pipeline
|
|
1720
|
+
* as an upgrade request of the adapter (guards, hooks, the route's checks), without socket. The
|
|
1721
|
+
* transport receives `{ kind: 'memory', peer }`. Resolves with the refusal, or `undefined` once
|
|
1722
|
+
* the connection was accepted. Available after `onBootstrap`.
|
|
1723
|
+
*/
|
|
1724
|
+
upgradeInMemory(request: InjectRequest, native: MemoryUpgrade): Promise<InjectResponse | undefined>;
|
|
1725
|
+
register(ctx: PluginContext): void;
|
|
1726
|
+
onBootstrap(ctx: PluginContext): void;
|
|
1727
|
+
onStart(ctx: PluginContext): Promise<void>;
|
|
1728
|
+
onStop(ctx: PluginContext): Promise<void>;
|
|
1729
|
+
onDispose(): Promise<void>;
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* A route served by `HttpPlugin` on behalf of another plugin. It runs through the same pipeline as
|
|
1733
|
+
* controller routes: `http:request` hook, request scope, global guards, then its own `guards`, the
|
|
1734
|
+
* handler, the exception mappers; global interceptors do not wrap it. The handler result is
|
|
1735
|
+
* serialised like a controller result (`HttpResponse`, objects as JSON, strings, streams...).
|
|
1736
|
+
*/
|
|
1737
|
+
interface PluginRoute {
|
|
1738
|
+
/** `GET` routes also answer `HEAD`; `ALL` registers every method without its own route. */
|
|
1739
|
+
readonly method: HttpMethod;
|
|
1740
|
+
/** Path relative to the `HttpPlugin` prefix, with the controller grammar (`/openapi.json`, `/docs/_assets/*`). */
|
|
1741
|
+
readonly path: string;
|
|
1742
|
+
/** Handler name, shown in `HttpPlugin.routes`, logs and errors (`document`). */
|
|
1743
|
+
readonly name: string;
|
|
1744
|
+
readonly handler: (ctx: HttpContext) => unknown;
|
|
1745
|
+
/**
|
|
1746
|
+
* Route metadata (`[Public.meta()]`): read by guards (`Public.get(ctx)`) and by the boot check of
|
|
1747
|
+
* required route decorator groups. One entry per group.
|
|
1748
|
+
*/
|
|
1749
|
+
readonly meta?: readonly RouteMetaEntry[];
|
|
1750
|
+
/** Guards of the route (provider classes or instances), run after the global guards. */
|
|
1751
|
+
readonly guards?: readonly GuardRef[];
|
|
1752
|
+
/**
|
|
1753
|
+
* Maximum body size of this route (`'100kb'`, `1048576`, `Infinity`), with the grammar of the
|
|
1754
|
+
* `HttpPlugin` option `bodyLimit`, which it replaces for this route only (lower or higher, like
|
|
1755
|
+
* `@BodyLimit` on a controller route). Past it, reading the body answers 413. Default: the
|
|
1756
|
+
* `HttpPlugin` option.
|
|
1757
|
+
*/
|
|
1758
|
+
readonly bodyLimit?: ByteSize;
|
|
1759
|
+
}
|
|
1760
|
+
/** What {@link PluginHttpRoutes.httpRoutes} receives. */
|
|
1761
|
+
interface PluginHttpRoutesContext {
|
|
1762
|
+
/** The `HttpPlugin` prefix (`''` or `/api`), already applied to the paths of the routes. */
|
|
1763
|
+
readonly prefix: string;
|
|
1764
|
+
/** The application container (initialised). */
|
|
1765
|
+
readonly container: Container;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Implemented by a plugin that serves HTTP routes. `HttpPlugin` calls `httpRoutes()` once in its
|
|
1769
|
+
* `onBootstrap`, after `container.init()` and whatever the plugin order, then registers the routes
|
|
1770
|
+
* on its adapter. A path that breaks the grammar, or that collides with another route, is ZNIGHT007
|
|
1771
|
+
* naming the plugin.
|
|
1772
|
+
*
|
|
1773
|
+
* ```ts
|
|
1774
|
+
* export class StatusPlugin implements Plugin, PluginHttpRoutes {
|
|
1775
|
+
* readonly name = 'status'
|
|
1776
|
+
* httpRoutes(): readonly PluginRoute[] {
|
|
1777
|
+
* return [{ method: 'GET', path: '/status', name: 'status', handler: () => ({ up: true }), meta: [Public.meta()] }]
|
|
1778
|
+
* }
|
|
1779
|
+
* }
|
|
1780
|
+
* ```
|
|
1781
|
+
*/
|
|
1782
|
+
interface PluginHttpRoutes {
|
|
1783
|
+
httpRoutes(context: PluginHttpRoutesContext): readonly PluginRoute[];
|
|
1784
|
+
/**
|
|
1785
|
+
* Called once when `HttpPlugin` starts draining (its `onStop`, whatever the plugin order), after
|
|
1786
|
+
* it stopped accepting connections and started closing its event streams: end the long-lived
|
|
1787
|
+
* responses `HttpPlugin` does not know of (a stream another server produced), so that the drain
|
|
1788
|
+
* does not wait for them until `drainTimeoutMs`. Requests still in flight are served. The drain
|
|
1789
|
+
* waits for a returned promise too (within `drainTimeoutMs`); a failure is logged.
|
|
1790
|
+
*/
|
|
1791
|
+
onHttpDrain?(): void | Promise<void>;
|
|
1792
|
+
/**
|
|
1793
|
+
* WebSocket upgrade routes of the plugin (RFC 0003, `@znight/websocket`): see
|
|
1794
|
+
* {@link PluginHttpUpgrades}. Called once in `onBootstrap`, after `httpRoutes()`.
|
|
1795
|
+
*/
|
|
1796
|
+
httpUpgrades?(context: PluginHttpUpgradesContext): PluginUpgrades;
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* The upgrade of a `node:http` server (`'upgrade'` event): every adapter built on `node:http`
|
|
1800
|
+
* (`node`, `express`, `fastify`, `hono` on Node.js), on Node.js and on Bun.
|
|
1801
|
+
*/
|
|
1802
|
+
interface NodeUpgrade {
|
|
1803
|
+
readonly kind: "node";
|
|
1804
|
+
/** The `IncomingMessage` of the handshake. */
|
|
1805
|
+
readonly request: unknown;
|
|
1806
|
+
/** The `net.Socket` (a `Duplex`) of the connection. */
|
|
1807
|
+
readonly socket: unknown;
|
|
1808
|
+
/** The first bytes after the handshake headers. */
|
|
1809
|
+
readonly head: Uint8Array;
|
|
1810
|
+
}
|
|
1811
|
+
/** The upgrade of a `Bun.serve()` server (`honoAdapter()` on Bun). */
|
|
1812
|
+
interface BunUpgrade {
|
|
1813
|
+
readonly kind: "bun";
|
|
1814
|
+
/** The Fetch `Request` of the handshake. */
|
|
1815
|
+
readonly request: Request;
|
|
1816
|
+
/** The `Bun.serve()` server (`server.upgrade(request, { data, headers })`). */
|
|
1817
|
+
readonly server: unknown;
|
|
1818
|
+
}
|
|
1819
|
+
/** An in-memory upgrade (`HttpPlugin.upgradeInMemory()`, tests): `peer` belongs to the caller. */
|
|
1820
|
+
interface MemoryUpgrade {
|
|
1821
|
+
readonly kind: "memory";
|
|
1822
|
+
readonly peer: unknown;
|
|
1823
|
+
}
|
|
1824
|
+
/** The native side of an upgrade request, completed by a transport. */
|
|
1825
|
+
type NativeUpgrade = NodeUpgrade | BunUpgrade | MemoryUpgrade;
|
|
1826
|
+
/** What {@link PluginUpgradeRoute.accept} receives to complete the handshake. */
|
|
1827
|
+
interface PendingUpgrade {
|
|
1828
|
+
readonly native: NativeUpgrade;
|
|
1829
|
+
/**
|
|
1830
|
+
* Headers of the `101` response: the headers and cookies the guards set on the context, the
|
|
1831
|
+
* request id and the security headers are not included (they are HTTP response headers); read
|
|
1832
|
+
* once, when the transport completes the handshake.
|
|
1833
|
+
*/
|
|
1834
|
+
readonly headers: Readonly<Record<string, HeaderValue>>;
|
|
1835
|
+
/**
|
|
1836
|
+
* The handshake timed out and was answered `408`: the transport must not complete it. Check it
|
|
1837
|
+
* after every `await`, right before completing the handshake.
|
|
1838
|
+
*/
|
|
1839
|
+
readonly expired: boolean;
|
|
1840
|
+
/**
|
|
1841
|
+
* Keeps the request scope (and the in-flight count of `HttpPlugin`) open until `closed`
|
|
1842
|
+
* settles: the connection lives in the scope of its handshake. Call it once, after the transport
|
|
1843
|
+
* completed the handshake.
|
|
1844
|
+
*/
|
|
1845
|
+
hold(closed: Promise<unknown>): void;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* A route answering upgrade requests (`GET` with `upgrade: websocket`), declared by another plugin
|
|
1849
|
+
* through {@link PluginHttpUpgrades.httpUpgrades}.
|
|
1850
|
+
*/
|
|
1851
|
+
interface PluginUpgradeRoute {
|
|
1852
|
+
/** Path relative to the `HttpPlugin` prefix, with the route grammar (`/chat`, `/docs/:docId`). */
|
|
1853
|
+
readonly path: string;
|
|
1854
|
+
/** Name of the route (the gateway), shown in `HttpPlugin.routes`, logs and errors. */
|
|
1855
|
+
readonly name: string;
|
|
1856
|
+
/** `ctx.route.controller` during the handshake (the gateway token). Default: the plugin name. */
|
|
1857
|
+
readonly owner?: Token;
|
|
1858
|
+
/** Route metadata (`ctx.route.meta`), one entry per group, checked at boot like plugin routes. */
|
|
1859
|
+
readonly meta?: readonly RouteMetaEntry[];
|
|
1860
|
+
/** Guards of the handshake (classes, instances or provider tokens), after the global guards. */
|
|
1861
|
+
readonly guards?: readonly (GuardRef | Token)[];
|
|
1862
|
+
/** Time the handshake gets before it is answered `408` (ms). Default: none. */
|
|
1863
|
+
readonly timeoutMs?: number;
|
|
1864
|
+
/**
|
|
1865
|
+
* Cheap checks before the guards (origin, subprotocols, capacity), after the protocol checks of
|
|
1866
|
+
* RFC 6455. Throw an `HttpException` to refuse.
|
|
1867
|
+
*/
|
|
1868
|
+
precheck?(ctx: HttpContext): void | Promise<void>;
|
|
1869
|
+
/** After the guards: take the connection. Throw an `HttpException` to refuse. */
|
|
1870
|
+
accept(ctx: HttpContext, upgrade: PendingUpgrade): void | Promise<void>;
|
|
1871
|
+
}
|
|
1872
|
+
/** What {@link PluginHttpUpgrades.httpUpgrades} returns. */
|
|
1873
|
+
interface PluginUpgrades {
|
|
1874
|
+
readonly routes: readonly PluginUpgradeRoute[];
|
|
1875
|
+
/**
|
|
1876
|
+
* Server-wide settings of the `websocket` option of `Bun.serve()` (`maxPayloadLength`,
|
|
1877
|
+
* `idleTimeout`, `backpressureLimit`, `sendPings`, `perMessageDeflate`...), used by adapters
|
|
1878
|
+
* serving with `Bun.serve()`. The handlers are set by `HttpPlugin`: they dispatch every event to
|
|
1879
|
+
* the `data` given to `server.upgrade()`, a {@link BunSocketHandlers}.
|
|
1880
|
+
*/
|
|
1881
|
+
readonly bunWebSocket?: Readonly<Record<string, unknown>>;
|
|
1882
|
+
}
|
|
1883
|
+
/** What {@link PluginHttpUpgrades.httpUpgrades} receives. */
|
|
1884
|
+
interface PluginHttpUpgradesContext extends PluginHttpRoutesContext {
|
|
1885
|
+
/** Name of the adapter (`node`, `fastify`...). */
|
|
1886
|
+
readonly adapter: string;
|
|
1887
|
+
/**
|
|
1888
|
+
* The native upgrades the adapter produces (`node`: a `node:http` server, `bun`: `Bun.serve()`),
|
|
1889
|
+
* `undefined` when the adapter cannot upgrade connections.
|
|
1890
|
+
*/
|
|
1891
|
+
readonly upgrades: "node" | "bun" | undefined;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Implemented, next to {@link PluginHttpRoutes}, by a plugin that accepts WebSocket connections
|
|
1895
|
+
* (`@znight/websocket`). `HttpPlugin` calls `httpUpgrades()` once in its `onBootstrap`, after
|
|
1896
|
+
* `httpRoutes()`, whatever the plugin order, and asks its adapter to route the matching upgrade
|
|
1897
|
+
* requests to the pipeline. `onHttpDrain()` is where the plugin closes its connections.
|
|
1898
|
+
*/
|
|
1899
|
+
interface PluginHttpUpgrades {
|
|
1900
|
+
httpUpgrades(context: PluginHttpUpgradesContext): PluginUpgrades;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* The `data` a transport gives to `server.upgrade()` on `Bun.serve()`: `HttpPlugin` dispatches
|
|
1904
|
+
* every event of the socket to it.
|
|
1905
|
+
*/
|
|
1906
|
+
interface BunSocketHandlers {
|
|
1907
|
+
open(ws: unknown): void;
|
|
1908
|
+
message(ws: unknown, message: string | Uint8Array): void;
|
|
1909
|
+
close(ws: unknown, code: number, reason: string): void;
|
|
1910
|
+
drain(ws: unknown): void;
|
|
1911
|
+
pong?(ws: unknown): void;
|
|
1912
|
+
}
|
|
1913
|
+
/** What `HttpPlugin` gives to {@link AdapterUpgrades.enable}. */
|
|
1914
|
+
interface UpgradeHandlerOptions {
|
|
1915
|
+
/** Whether an upgrade request to the raw `path` (no query string) is for Znight. */
|
|
1916
|
+
match(path: string): boolean;
|
|
1917
|
+
/**
|
|
1918
|
+
* Runs the handshake through the pipeline. Resolves with the refusal to write (an HTTP response),
|
|
1919
|
+
* or `undefined` once the transport took the connection. Never rejects.
|
|
1920
|
+
*/
|
|
1921
|
+
handle(request: AdapterRequest, native: NodeUpgrade | BunUpgrade): Promise<AdapterReply | undefined>;
|
|
1922
|
+
/** The `websocket` option of `Bun.serve()` (settings and dispatching handlers), Bun adapters only. */
|
|
1923
|
+
readonly bunWebSocket: Readonly<Record<string, unknown>>;
|
|
1924
|
+
}
|
|
1925
|
+
/** The optional upgrade capability of an adapter (RFC 0003 §8.2.3). */
|
|
1926
|
+
interface AdapterUpgrades {
|
|
1927
|
+
/** The native upgrades the adapter produces. */
|
|
1928
|
+
readonly kind: "node" | "bun";
|
|
1929
|
+
/** Called once, before `listen()`, when the application has upgrade routes. */
|
|
1930
|
+
enable(options: UpgradeHandlerOptions): void;
|
|
1931
|
+
}
|
|
1932
|
+
/** A request, as seen by the plugin. */
|
|
1933
|
+
interface AdapterRequest {
|
|
1934
|
+
/** Upper-case method. */
|
|
1935
|
+
readonly method: string;
|
|
1936
|
+
/** Raw request target: path and query string, percent-encoded (`/users/a%20b?page=2`). */
|
|
1937
|
+
readonly url: string;
|
|
1938
|
+
/** Request headers with lower-case names (repeated headers joined with `, `). */
|
|
1939
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
1940
|
+
/**
|
|
1941
|
+
* Client address, when known: the socket peer, or the address the server framework derives from
|
|
1942
|
+
* its own proxy settings (Fastify `trustProxy`, Express `trust proxy`). This is `ctx.ip` unless
|
|
1943
|
+
* the plugin option `trustProxy` is set.
|
|
1944
|
+
*/
|
|
1945
|
+
readonly ip: string | undefined;
|
|
1946
|
+
/**
|
|
1947
|
+
* Address of the socket peer, **never** taken from headers: where the plugin starts walking
|
|
1948
|
+
* `x-forwarded-for` with `trustProxy`. Optional (`ip` is used without it); the built-in adapters
|
|
1949
|
+
* set it.
|
|
1950
|
+
*/
|
|
1951
|
+
readonly remoteAddress?: string | undefined;
|
|
1952
|
+
/** `true` when the connection uses TLS (`ctx.protocol` without a trusted proxy). Optional. */
|
|
1953
|
+
readonly encrypted?: boolean;
|
|
1954
|
+
/** Native request object (`@Req()`). */
|
|
1955
|
+
readonly raw: unknown;
|
|
1956
|
+
/** Native response object (`@Res()`). */
|
|
1957
|
+
readonly rawResponse: unknown;
|
|
1958
|
+
/**
|
|
1959
|
+
* Reads the whole body (once). Rejects with a `PayloadTooLargeException` when it exceeds `limit`
|
|
1960
|
+
* bytes (checked on `content-length` first, then while reading).
|
|
1961
|
+
*/
|
|
1962
|
+
readBody(limit: number): Promise<Uint8Array>;
|
|
1963
|
+
/**
|
|
1964
|
+
* The body as a Web stream, read as the consumer pulls it (once; exclusive with `readBody`). It
|
|
1965
|
+
* errors with a `PayloadTooLargeException` past `limit` bytes (`content-length` checked first) and
|
|
1966
|
+
* with a `BadRequestException` when the client aborts. Cancelling it discards the rest of the body
|
|
1967
|
+
* without closing the connection before the response is written. Optional: without it the plugin
|
|
1968
|
+
* streams the result of `readBody` (multipart uploads are then buffered before being parsed).
|
|
1969
|
+
*/
|
|
1970
|
+
streamBody?(limit: number): ReadableStream<Uint8Array>;
|
|
1971
|
+
/**
|
|
1972
|
+
* Whether the request has a body, asked only when its headers declare none (neither
|
|
1973
|
+
* `content-length` nor `transfer-encoding`: Web `Request`s built in memory). Return `false` for
|
|
1974
|
+
* requests read from an HTTP/1.1 connection (without those headers they have no body). Optional:
|
|
1975
|
+
* no body is assumed otherwise.
|
|
1976
|
+
*/
|
|
1977
|
+
hasBody?(): boolean;
|
|
1978
|
+
/** Status written by a handler that took over the response with `@Res()`, when known. */
|
|
1979
|
+
rawStatus?(): number | undefined;
|
|
1980
|
+
/**
|
|
1981
|
+
* Registers a listener called once when the client disconnects before the response was fully
|
|
1982
|
+
* written. When it already happened, the listener is called on the next microtask. Never called
|
|
1983
|
+
* after a complete response. Optional: without it, `ctx.signal` only aborts when the response
|
|
1984
|
+
* body is cancelled or an event stream is closed by the server.
|
|
1985
|
+
*/
|
|
1986
|
+
onAbort?(listener: () => void): void;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Body of a reply: bytes, a stream or nothing. Adapters MUST either read a stream to its end or
|
|
1990
|
+
* cancel it (client disconnected, response not sent...): the request scope and the in-flight count
|
|
1991
|
+
* are released then. A `Blob` is sent with its size; the plugin itself never passes one: it sends
|
|
1992
|
+
* files as a stream reading the `Blob`, given in {@link AdapterReply.blob}.
|
|
1993
|
+
*/
|
|
1994
|
+
type ReplyBody = Uint8Array | ReadableStream<Uint8Array> | Blob | null;
|
|
1995
|
+
/** A response computed by the plugin, written by the adapter. */
|
|
1996
|
+
interface AdapterReply {
|
|
1997
|
+
readonly status: number;
|
|
1998
|
+
/** Response headers, lower-case names (validated: every name and value can be sent). */
|
|
1999
|
+
readonly headers: Readonly<Record<string, HeaderValue>>;
|
|
2000
|
+
/** `null` for HEAD requests and bodyless statuses (headers such as `content-length` are kept). */
|
|
2001
|
+
readonly body: ReplyBody;
|
|
2002
|
+
/**
|
|
2003
|
+
* The text `body` holds encoded as UTF-8, for a body serialised from a string (JSON, text).
|
|
2004
|
+
* Adapters writing strings MAY send it instead of `body`, which writes the same bytes: Node.js then
|
|
2005
|
+
* writes the headers and the body at once (bytes take a second write).
|
|
2006
|
+
*/
|
|
2007
|
+
readonly text?: string;
|
|
2008
|
+
/**
|
|
2009
|
+
* The `Blob` (file) that `body` streams, for adapters able to send it natively (`sendfile`, exact
|
|
2010
|
+
* length: `new Response(blob)` on `Bun.serve()`). An adapter sending it instead of reading `body`
|
|
2011
|
+
* MUST cancel `body` once the server holds the blob (file opened): that releases the request
|
|
2012
|
+
* scope, which may delete the file. Headers (`content-length`...) are the same either way.
|
|
2013
|
+
*/
|
|
2014
|
+
readonly blob?: Blob;
|
|
2015
|
+
/** The handler wrote the response itself through `@Res()`: write nothing. */
|
|
2016
|
+
readonly raw?: boolean;
|
|
2017
|
+
/**
|
|
2018
|
+
* The body is a long-lived event stream (a streamed `text/event-stream` body: `@Sse`,
|
|
2019
|
+
* `HttpResponse.sse()`, a proxied `fetch()` response...). Adapters MUST send the headers without
|
|
2020
|
+
* waiting for the first chunk (a quiet stream may send nothing for a while), MUST NOT apply idle or
|
|
2021
|
+
* inactivity timeouts to the response while it is sent, and MUST write each chunk as soon as it is
|
|
2022
|
+
* read (no buffering, no compression).
|
|
2023
|
+
*/
|
|
2024
|
+
readonly eventStream?: boolean;
|
|
2025
|
+
}
|
|
2026
|
+
/** Handles one request. Never rejects: errors are already mapped to a reply. */
|
|
2027
|
+
type AdapterHandler = (request: AdapterRequest) => Promise<AdapterReply>;
|
|
2028
|
+
/**
|
|
2029
|
+
* Handles requests no route matched (`404`/`405`, automatic `OPTIONS`) and errors raised by the
|
|
2030
|
+
* server framework before a route handler ran (`error`, e.g. a malformed URL). Never rejects.
|
|
2031
|
+
*/
|
|
2032
|
+
type FallbackHandler = (request: AdapterRequest, error?: unknown) => Promise<AdapterReply>;
|
|
2033
|
+
/** A concrete route to register (the plugin expands `ALL` and adds `HEAD` for `GET` routes). */
|
|
2034
|
+
interface AdapterRoute {
|
|
2035
|
+
readonly method: Exclude<HttpMethod, "ALL">;
|
|
2036
|
+
/** Parsed full path (plugin prefix included). Translate it with the helpers of `@znight/http/path`. */
|
|
2037
|
+
readonly path: RoutePath;
|
|
2038
|
+
readonly handler: AdapterHandler;
|
|
2039
|
+
}
|
|
2040
|
+
/** Address of a listening server. Converts to its URL (`String(address)`). */
|
|
2041
|
+
interface HttpServerAddress {
|
|
2042
|
+
/** Host the server is bound to (`0.0.0.0`, `::`, `127.0.0.1`...). */
|
|
2043
|
+
readonly host: string;
|
|
2044
|
+
readonly port: number;
|
|
2045
|
+
/** `IPv4` or `IPv6`. */
|
|
2046
|
+
readonly family: string;
|
|
2047
|
+
/** Base URL usable by clients (`http://127.0.0.1:3000`; wildcard hosts become loopback addresses). */
|
|
2048
|
+
readonly url: string;
|
|
2049
|
+
toString(): string;
|
|
2050
|
+
}
|
|
2051
|
+
/** A request for {@link HttpAdapter.inject} (in-memory, no network). */
|
|
2052
|
+
interface InjectRequest {
|
|
2053
|
+
readonly method: string;
|
|
2054
|
+
/** Path and optional query string (`/users?page=2`). */
|
|
2055
|
+
readonly url: string;
|
|
2056
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
2057
|
+
/**
|
|
2058
|
+
* Request body: a string or bytes are sent as is (`text/plain` / `application/octet-stream` when
|
|
2059
|
+
* no `content-type` is given), a `FormData` as `multipart/form-data` (files included), a `Blob` /
|
|
2060
|
+
* `File` with its type, anything else as JSON (`application/json`).
|
|
2061
|
+
*/
|
|
2062
|
+
readonly body?: unknown;
|
|
2063
|
+
/** Query parameters appended to `url`. */
|
|
2064
|
+
readonly query?: Readonly<Record<string, string | readonly string[]>>;
|
|
2065
|
+
/**
|
|
2066
|
+
* Request cookies (name → value), sent in a `cookie` header after any `cookie` header of
|
|
2067
|
+
* `headers`. Values are encoded like the values of `serializeCookie`, so a value read from
|
|
2068
|
+
* `response.cookies` can be sent back as is (signed values included).
|
|
2069
|
+
*/
|
|
2070
|
+
readonly cookies?: Readonly<Record<string, string>>;
|
|
2071
|
+
/**
|
|
2072
|
+
* Cookies signed by `HttpPlugin` with its first secret (`cookies.secrets`) and merged into
|
|
2073
|
+
* `cookies` before dispatch: the way to send a valid `@SignedCookie`. Adapters never receive this
|
|
2074
|
+
* field.
|
|
2075
|
+
*/
|
|
2076
|
+
readonly signedCookies?: Readonly<Record<string, string>>;
|
|
2077
|
+
}
|
|
2078
|
+
/** A request for {@link HttpAdapter.injectStream}: an {@link InjectRequest} that can be aborted. */
|
|
2079
|
+
interface InjectStreamRequest extends InjectRequest {
|
|
2080
|
+
/** Aborting it disconnects the client (like cancelling the response body). */
|
|
2081
|
+
readonly signal?: AbortSignal;
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* A request for `HttpPlugin.injectStream()` / `injectSse()`: an {@link InjectStreamRequest} whose
|
|
2085
|
+
* method defaults to `GET`.
|
|
2086
|
+
*/
|
|
2087
|
+
type InjectStreamOptions = Omit<InjectStreamRequest, "method"> & {
|
|
2088
|
+
readonly method?: string;
|
|
2089
|
+
};
|
|
2090
|
+
/** The response of {@link HttpAdapter.inject}. */
|
|
2091
|
+
interface InjectResponse {
|
|
2092
|
+
readonly status: number;
|
|
2093
|
+
/**
|
|
2094
|
+
* Lower-case header names; repeated headers are joined with `, `, except `set-cookie`: an array
|
|
2095
|
+
* with one entry per cookie (cookie values may contain `, `).
|
|
2096
|
+
*/
|
|
2097
|
+
readonly headers: Record<string, string | string[]>;
|
|
2098
|
+
/** Body decoded as UTF-8. */
|
|
2099
|
+
readonly body: string;
|
|
2100
|
+
/** Raw body bytes (binary downloads). */
|
|
2101
|
+
readonly bytes: Uint8Array;
|
|
2102
|
+
/** `JSON.parse(body)`. */
|
|
2103
|
+
json<T = unknown>(): T;
|
|
2104
|
+
/** `set-cookie` parsed, by cookie name (the last one wins). */
|
|
2105
|
+
readonly cookies: Readonly<Record<string, SetCookieInfo>>;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* The response of {@link HttpAdapter.injectStream}: available as soon as the headers are sent, its
|
|
2109
|
+
* body read as it is produced.
|
|
2110
|
+
*/
|
|
2111
|
+
interface InjectStreamResponse {
|
|
2112
|
+
readonly status: number;
|
|
2113
|
+
/** Like {@link InjectResponse.headers} (`set-cookie` as an array). */
|
|
2114
|
+
readonly headers: Record<string, string | string[]>;
|
|
2115
|
+
readonly cookies: Readonly<Record<string, SetCookieInfo>>;
|
|
2116
|
+
/** The body as it is produced. Cancelling it disconnects the client (the server sees `onAbort` and a cancelled reply body). */
|
|
2117
|
+
readonly body: ReadableStream<Uint8Array>;
|
|
2118
|
+
}
|
|
2119
|
+
/** Options of {@link HttpAdapter.close}. */
|
|
2120
|
+
interface AdapterCloseOptions {
|
|
2121
|
+
/** Destroy open connections instead of waiting for in-flight requests. */
|
|
2122
|
+
readonly force?: boolean;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* A server implementation. Create one with `nodeAdapter()` (default), `fastifyAdapter()`,
|
|
2126
|
+
* `expressAdapter()` or `honoAdapter()`. An adapter instance serves one plugin.
|
|
2127
|
+
*/
|
|
2128
|
+
interface HttpAdapter {
|
|
2129
|
+
/** `node`, `fastify`, `express`, `hono`... (exposed as `ctx.adapter`). */
|
|
2130
|
+
readonly name: string;
|
|
2131
|
+
/** Registers a route. Called during `onBootstrap`, before `listen`/`inject`. */
|
|
2132
|
+
registerRoute(route: AdapterRoute): void;
|
|
2133
|
+
/** Sets the handler of unmatched requests and framework errors. Called once, before `listen`/`inject`. */
|
|
2134
|
+
setFallbackHandler(handler: FallbackHandler): void;
|
|
2135
|
+
/** Starts listening. `port` 0 picks a free port. */
|
|
2136
|
+
listen(port: number, host: string): Promise<HttpServerAddress>;
|
|
2137
|
+
/**
|
|
2138
|
+
* Stops accepting connections, closes idle ones and resolves once in-flight requests completed and
|
|
2139
|
+
* every connection is closed (`force`: destroys them). Idempotent; also releases what the adapter
|
|
2140
|
+
* created when it never listened.
|
|
2141
|
+
*/
|
|
2142
|
+
close(options?: AdapterCloseOptions): Promise<void>;
|
|
2143
|
+
/**
|
|
2144
|
+
* Sends a request in memory (no socket), through the same routing and pipeline, and reads the
|
|
2145
|
+
* whole response. The built-in adapters reject an event stream (`text/event-stream`, which would
|
|
2146
|
+
* never end) with a hint to `injectSse()` / `injectStream()`.
|
|
2147
|
+
*/
|
|
2148
|
+
inject(request: InjectRequest): Promise<InjectResponse>;
|
|
2149
|
+
/**
|
|
2150
|
+
* In-memory request whose response body is returned as it is produced (resolves at the headers).
|
|
2151
|
+
* Cancelling `body` (or aborting `signal`) disconnects the client. Optional for third-party
|
|
2152
|
+
* adapters; the four built-in adapters implement it.
|
|
2153
|
+
*/
|
|
2154
|
+
injectStream?(request: InjectStreamRequest): Promise<InjectStreamResponse>;
|
|
2155
|
+
/**
|
|
2156
|
+
* WebSocket upgrades (RFC 0003): the adapter routes the upgrade requests `HttpPlugin` asks for
|
|
2157
|
+
* to the pipeline, before its own routing. Optional: an adapter without it cannot serve
|
|
2158
|
+
* `@znight/websocket` gateways. The four built-in adapters implement it.
|
|
2159
|
+
*/
|
|
2160
|
+
readonly upgrades?: AdapterUpgrades;
|
|
2161
|
+
}
|
|
2162
|
+
export { BodyLimit as $, InferSchemaOutput as $n, ContentDispositionType as $t, ExceptionMapperRef as A, ConflictException as An, Validate as At, HstsOptions as B, NotFoundException as Bn, HttpInterceptorClass as Bt, PluginHttpUpgradesContext as C, ByteSize as Cn, RouteMetaEntry as Ct, PluginHttpRoutes as D, parseQuery as Dn, UploadedFiles as Dt, UpgradeHandlerOptions as E, QueryRecord as En, UploadedFile as Et, HttpRouteSummary as F, HttpErrorBody as Fn, ExceptionMapperClass as Ft, ParseSseOptions as G, UnauthorizedException as Gn, isHttpResponse as Gt, TrustProxy as H, PayloadTooLargeException as Hn, HttpResponseInit as Ht, HttpSseOptions as I, HttpException as In, ExceptionMapperResult as It, parseSseStream as J, ValidationException as Jn, SseEvent as Jt, SseItem as K, UnprocessableEntityException as Kn, StreamSource as Kt, InterceptorRef as L, HttpExceptionOptions as Ln, GuardClass as Lt, HttpCookieOptions as M, GatewayTimeoutException as Mn, CallHandler as Mt, HttpPlugin as N, GoneException as Nn, CanActivate as Nt, PluginHttpRoutesContext as O, BadGatewayException as On, UseGuard as Ot, HttpPluginOptions as P, HeaderValue as Pn, ExceptionMapper as Pt, Body as Q, InferSchemaInput as Qn, ByteRange as Qt, TrailingSlashPolicy as R, InternalServerErrorException as Rn, GuardEnforcement as Rt, PluginHttpUpgrades as S, uploads as Sn, RouteMetaCarrier as St, PluginUpgrades as T, parseByteSize as Tn, Sse as Tt, InjectSseResponse as U, ServiceUnavailableException as Un, RedirectStatus as Ut, SecurityHeadersOptions as V, NotImplementedException as Vn, HttpResponse as Vt, InjectSseWaitOptions as W, TooManyRequestsException as Wn, StreamResponseOptions as Wt, CorsOrigin as X, isHttpException as Xn, SseSource as Xt, CorsOptions as Y, ValidationIssueDetail as Yn, SseOptions as Yt, All as Z, toIssueDetail as Zn, encodeSseEvent as Zt, BunUpgrade as _, RequestId as _n, serializeClearCookie as _r, Res as _t, AdapterRoute as a, HttpMetadata as an, StandardSchemaV1Props as ar, Get as at, NodeUpgrade as b, UploadedFilesOptions as bn, RouteDecoratorInfo as bt, HttpServerAddress as c, HttpParamSource as cn, StandardSchemaV1Types as cr, Headers as ct, InjectStreamOptions as d, HttpSseDefinition as dn, CookieDefaults as dr, Param as dt, FileBody as en, StandardSchemaV1 as er, Controller as et, InjectStreamRequest as f, HttpUploadDefinition as fn, CookieOptions as fr, Patch as ft, BunSocketHandlers as g, RequestAbortedError as gn, parseSetCookie as gr, Req as gt, AdapterUpgrades as h, RequestAbortKind as hn, parseCookieHeader as hr, Query as ht, AdapterRequest as i, HttpControllerDefinition as in, StandardSchemaV1PathSegment as ir, Delete as it, GuardRef as j, ForbiddenException as jn, createRouteDecorator as jt, PluginRoute as k, BadRequestException as kn, UseInterceptor as kt, InjectRequest as l, HttpRouteDefinition as ln, isStandardSchema as lr, HttpCode as lt, ReplyBody as m, HttpRouteInfo as mn, SetCookieInfo as mr, Put as mt, AdapterHandler as n, contentDisposition as nn, StandardSchemaV1Issue as nr, Cookie as nt, FallbackHandler as o, HttpMethod as on, StandardSchemaV1Result as or, Head as ot, InjectStreamResponse as p, HttpContext as pn, ResponseCookie as pr, Post as pt, SseMessage as q, UnsupportedMediaTypeException as qn, SseBody as qt, AdapterReply as r, parseRange as rn, StandardSchemaV1Options as rr, Ctx as rt, HttpAdapter as s, HttpParamDefinition as sn, StandardSchemaV1SuccessResult as sr, Header as st, AdapterCloseOptions as t, FileResponseOptions as tn, StandardSchemaV1FailureResult as tr, ControllerOptions as tt, InjectResponse as u, HttpRouteGroupDefinition as un, ClearCookieOptions as ur, Options as ut, MemoryUpgrade as v, UploadLimits as vn, serializeCookie as vr, RouteDecorator as vt, PluginUpgradeRoute as w, formatByteSize as wn, SignedCookie as wt, PendingUpgrade as x, Uploads as xn, RouteDecoratorOptions as xt, NativeUpgrade as y, UploadedFileOptions as yn, RouteDecoratorArgs as yt, EnforcementMode as z, MethodNotAllowedException as zn, HttpInterceptor as zt };
|
|
2163
|
+
//# sourceMappingURL=adapter-6qC95ViO.d.mts.map
|