@znight/http 0.5.1 → 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
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { n as RouteTable, t as nodeAdapter } from "./node-
|
|
2
|
-
import {
|
|
1
|
+
import { n as RouteTable, t as nodeAdapter } from "./node-npGnu7IW.mjs";
|
|
2
|
+
import { A as Req, At as ConflictException, B as isGroupOf, Bt as ServiceUnavailableException, C as HttpCode, Ct as discardStream, D as Post, Dt as trackStream, E as Patch, Et as toByteStream, F as UploadedFiles, Ft as InternalServerErrorException, Gt as ValidationException, Ht as UnauthorizedException, I as UseGuard, It as MethodNotAllowedException, Jt as toIssueDetail, Kt as isHttpException, L as UseInterceptor, Lt as NotFoundException, M as SignedCookie, Mt as GatewayTimeoutException, N as Sse, Nt as GoneException, O as Put, Ot as BadGatewayException, P as UploadedFile, Pt as HttpException, R as Validate, Rt as NotImplementedException, S as Headers, St as captureContext, T as Param, Tt as isStreamSource, Ut as UnprocessableEntityException, Vt as TooManyRequestsException, Wt as UnsupportedMediaTypeException, Xt as isValidStatus, Yt as isBodylessStatus, Zt as statusText, _ as Delete, _t as parseCookieHeader, b as Head, bt as serializeCookie, c as checkHandshake, d as All, dt as DEFAULT_COOKIE_OPTIONS, f as Body, ft as checkCookie, g as Ctx, gt as isCookieName, h as Cookie, ht as givenCookieKey, j as Res, jt as ForbiddenException, k as Query, kt as BadRequestException, l as collectPluginUpgrades, m as Controller, mt as encodeCookieValue, n as MemoryUpgradeRequest, nt as injectResponse, o as replyBytes, ot as normalizeInject, p as BodyLimit, pt as decodeCookieValue, qt as issue, rt as isEventStreamType, s as PendingUpgradeState, v as GROUP_NAME, vt as parseSetCookie, w as Options, wt as isByteStream, x as Header, xt as serializeResponseCookie, y as Get, yt as serializeClearCookie, z as createRouteDecorator, zt as PayloadTooLargeException } from "./upgrade-io-dTMDDmBi.mjs";
|
|
3
|
+
import { RoutePathError, compareRoutePaths, compareStrings, decodePathValue, joinPaths, matchRoutePath, normalizePath, parseRoutePath } from "./path.mjs";
|
|
3
4
|
import { a as isMediaTypePattern, i as contentTypeFor, n as parseByteSize, o as matchesMediaType, r as OCTET_STREAM, s as mediaTypeEssence, t as formatByteSize } from "./size-D8rsQcV0.mjs";
|
|
4
5
|
import { a as isThenable, i as isBun, n as errorMessage, r as fastRecord, t as emptyRecord } from "./utils-DIWdzVht.mjs";
|
|
5
|
-
import { RoutePathError, compareRoutePaths, compareStrings, decodePathValue, joinPaths, matchRoutePath, normalizePath, parseRoutePath } from "./path.mjs";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
|
+
import { ErrorCode, ZnightError, displayName, resolveOptions } from "@znight/core";
|
|
7
8
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
8
9
|
import { openAsBlob } from "node:fs";
|
|
9
10
|
import { realpath, stat } from "node:fs/promises";
|
|
10
11
|
import { basename, isAbsolute, relative, resolve } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { ErrorCode, ZnightError, displayName, resolveOptions } from "@znight/core";
|
|
13
13
|
import crypto$1, { createHmac, createSecretKey } from "node:crypto";
|
|
14
14
|
// src/multipart.ts
|
|
15
15
|
/** Default maximum size of the headers of one part. */
|
|
@@ -1169,252 +1169,6 @@ const EMPTY_HEADERS = Object.freeze({});
|
|
|
1169
1169
|
const NO_COOKIES$1 = Object.freeze([]);
|
|
1170
1170
|
const NO_LINES = Object.freeze([]);
|
|
1171
1171
|
const NO_FILES$1 = Object.freeze(emptyRecord());
|
|
1172
|
-
// src/decorators.ts
|
|
1173
|
-
function marker() {}
|
|
1174
|
-
const classMarker = marker;
|
|
1175
|
-
const methodMarker = marker;
|
|
1176
|
-
const parameterMarker = marker;
|
|
1177
|
-
const classOrMethodMarker = marker;
|
|
1178
|
-
/**
|
|
1179
|
-
* Declares a controller: a provider (singleton by default) whose decorated methods are routes.
|
|
1180
|
-
*
|
|
1181
|
-
* ```ts
|
|
1182
|
-
* @Controller('/users')
|
|
1183
|
-
* export class UserController {
|
|
1184
|
-
* constructor(private readonly users: UserService) {}
|
|
1185
|
-
*
|
|
1186
|
-
* @Get('/:id')
|
|
1187
|
-
* find(@Param('id') id: number) {
|
|
1188
|
-
* return this.users.findById(id)
|
|
1189
|
-
* }
|
|
1190
|
-
* }
|
|
1191
|
-
* ```
|
|
1192
|
-
*
|
|
1193
|
-
* @param pathOrOptions static: base path, or `{ path?, scope? }`.
|
|
1194
|
-
*/
|
|
1195
|
-
function Controller(_pathOrOptions) {
|
|
1196
|
-
return classMarker;
|
|
1197
|
-
}
|
|
1198
|
-
/** `GET` route. `path` (static) is relative to the controller path; default: the controller path. */
|
|
1199
|
-
function Get(_path) {
|
|
1200
|
-
return methodMarker;
|
|
1201
|
-
}
|
|
1202
|
-
/** `POST` route (default status 201). */
|
|
1203
|
-
function Post(_path) {
|
|
1204
|
-
return methodMarker;
|
|
1205
|
-
}
|
|
1206
|
-
/** `PUT` route. */
|
|
1207
|
-
function Put(_path) {
|
|
1208
|
-
return methodMarker;
|
|
1209
|
-
}
|
|
1210
|
-
/** `PATCH` route. */
|
|
1211
|
-
function Patch(_path) {
|
|
1212
|
-
return methodMarker;
|
|
1213
|
-
}
|
|
1214
|
-
/** `DELETE` route. */
|
|
1215
|
-
function Delete(_path) {
|
|
1216
|
-
return methodMarker;
|
|
1217
|
-
}
|
|
1218
|
-
/** `HEAD` route (`GET` routes already answer `HEAD` requests). */
|
|
1219
|
-
function Head(_path) {
|
|
1220
|
-
return methodMarker;
|
|
1221
|
-
}
|
|
1222
|
-
/** `OPTIONS` route (paths without one answer `OPTIONS` with `204` and an `allow` header). */
|
|
1223
|
-
function Options(_path) {
|
|
1224
|
-
return methodMarker;
|
|
1225
|
-
}
|
|
1226
|
-
/** Route for GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS (methods without a dedicated route on the same path). */
|
|
1227
|
-
function All(_path) {
|
|
1228
|
-
return methodMarker;
|
|
1229
|
-
}
|
|
1230
|
-
function Sse(_pathOrOptions, _options) {
|
|
1231
|
-
return methodMarker;
|
|
1232
|
-
}
|
|
1233
|
-
function Param(_nameOrSchema, _schema) {
|
|
1234
|
-
return parameterMarker;
|
|
1235
|
-
}
|
|
1236
|
-
function Query(_nameOrSchema, _schema) {
|
|
1237
|
-
return parameterMarker;
|
|
1238
|
-
}
|
|
1239
|
-
function Cookie(_nameOrSchema, _schema) {
|
|
1240
|
-
return parameterMarker;
|
|
1241
|
-
}
|
|
1242
|
-
/**
|
|
1243
|
-
* Injects the **verified** value of a signed cookie (set with `{ signed: true }`, plugin option
|
|
1244
|
-
* `cookies.secrets`): absent, tampered or signed with a retired key, the cookie does not exist
|
|
1245
|
-
* (400 `Missing cookie "name"` unless the parameter is optional). Same coercion and schema rules as
|
|
1246
|
-
* {@link Cookie}. A route reading a signed cookie fails the boot when `cookies.secrets` is not set.
|
|
1247
|
-
*
|
|
1248
|
-
* @param name static: cookie name.
|
|
1249
|
-
* @param schema value: a Standard Schema (its output is injected).
|
|
1250
|
-
*/
|
|
1251
|
-
function SignedCookie(_name, _schema) {
|
|
1252
|
-
return parameterMarker;
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Injects the parsed request body, validated with `schema` when given (the schema output is
|
|
1256
|
-
* injected: defaults, transforms, stripped keys). A non-primitive body without schema is reported
|
|
1257
|
-
* by the compiler (ZNIGHT017).
|
|
1258
|
-
*
|
|
1259
|
-
* @param schema value: a Standard Schema (Zod, Valibot, ArkType...).
|
|
1260
|
-
*/
|
|
1261
|
-
function Body(_schema) {
|
|
1262
|
-
return parameterMarker;
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Injects a request header (case-insensitive `name`, coerced to the parameter type) or, without
|
|
1266
|
-
* name, every header (`Record<string, string | undefined>`).
|
|
1267
|
-
*
|
|
1268
|
-
* @param name static: header name.
|
|
1269
|
-
*/
|
|
1270
|
-
function Headers(_name) {
|
|
1271
|
-
return parameterMarker;
|
|
1272
|
-
}
|
|
1273
|
-
function UploadedFile(_fieldOrOptions, _options) {
|
|
1274
|
-
return parameterMarker;
|
|
1275
|
-
}
|
|
1276
|
-
function UploadedFiles(_fieldOrOptions, _options) {
|
|
1277
|
-
return parameterMarker;
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Sets the maximum request body size of a route, or of every route of a controller (the route wins),
|
|
1281
|
-
* over the plugin `bodyLimit` (and over the limit computed for upload routes). `Infinity` disables
|
|
1282
|
-
* the limit, e.g. for a route streaming its body with `ctx.bodyStream()`.
|
|
1283
|
-
*
|
|
1284
|
-
* ```ts
|
|
1285
|
-
* @Put('/files/:name')
|
|
1286
|
-
* @BodyLimit('5gb')
|
|
1287
|
-
* async store(@Param('name') name: string, @Ctx() ctx: HttpContext) {
|
|
1288
|
-
* await pipeline(Readable.fromWeb(ctx.bodyStream()), createWriteStream(join(DIR, safeName(name))))
|
|
1289
|
-
* }
|
|
1290
|
-
* ```
|
|
1291
|
-
*
|
|
1292
|
-
* @param size static: bytes (`10_485_760`) or a size with a unit (`'10mb'`, `'512kb'`; `kb` = 1024 bytes).
|
|
1293
|
-
*/
|
|
1294
|
-
function BodyLimit(_size) {
|
|
1295
|
-
return classOrMethodMarker;
|
|
1296
|
-
}
|
|
1297
|
-
/** Injects the native request of the adapter (escape hatch; the compiler warns). */
|
|
1298
|
-
function Req() {
|
|
1299
|
-
return parameterMarker;
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
|
-
* Injects the native response of the adapter (escape hatch; the compiler warns). The handler then
|
|
1303
|
-
* writes the response itself, and must have finished when its promise settles.
|
|
1304
|
-
*/
|
|
1305
|
-
function Res() {
|
|
1306
|
-
return parameterMarker;
|
|
1307
|
-
}
|
|
1308
|
-
/** Injects the {@link HttpContext} of the request. */
|
|
1309
|
-
function Ctx() {
|
|
1310
|
-
return parameterMarker;
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* Sets the status of successful responses (wins over `HttpResponse.status` and the defaults).
|
|
1314
|
-
*
|
|
1315
|
-
* @param status static: an integer between 100 and 599.
|
|
1316
|
-
*/
|
|
1317
|
-
function HttpCode(_status) {
|
|
1318
|
-
return methodMarker;
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Adds a header to successful responses. Repeat the decorator for several headers.
|
|
1322
|
-
*
|
|
1323
|
-
* @param name static: header name.
|
|
1324
|
-
* @param value static: header value.
|
|
1325
|
-
*/
|
|
1326
|
-
function Header(_name, _value) {
|
|
1327
|
-
return methodMarker;
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Protects a controller (every route) or a route with guards (providers implementing
|
|
1331
|
-
* {@link CanActivate}). Order: global guards → controller guards → route guards, each list in
|
|
1332
|
-
* declaration order.
|
|
1333
|
-
*
|
|
1334
|
-
* @param guards value: guard classes.
|
|
1335
|
-
*/
|
|
1336
|
-
function UseGuard(..._guards) {
|
|
1337
|
-
return classOrMethodMarker;
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* Wraps a controller (every route) or a route with interceptors (providers implementing
|
|
1341
|
-
* {@link HttpInterceptor}). Order: global → controller → route, the first one being the outermost.
|
|
1342
|
-
*
|
|
1343
|
-
* @param interceptors value: interceptor classes.
|
|
1344
|
-
*/
|
|
1345
|
-
function UseInterceptor(..._interceptors) {
|
|
1346
|
-
return classOrMethodMarker;
|
|
1347
|
-
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Validates the request body with `schema`: sugar for `@Body(schema)` on the body parameter (using
|
|
1350
|
-
* both is ZNIGHT005).
|
|
1351
|
-
*
|
|
1352
|
-
* @param schema value: a Standard Schema.
|
|
1353
|
-
*/
|
|
1354
|
-
function Validate(_schema) {
|
|
1355
|
-
return methodMarker;
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Grammar of an explicit decision group name (internal; the compiler extension has the same rule).
|
|
1359
|
-
* A decorator without `group` is its own group, named after its key: any non-empty key is valid.
|
|
1360
|
-
*/
|
|
1361
|
-
const GROUP_NAME = /^[A-Za-z][\w.-]*$/;
|
|
1362
|
-
/** `true` when `group` is a valid group of `key`: the key itself (the default) or a valid name. */
|
|
1363
|
-
function isGroupOf(group, key) {
|
|
1364
|
-
return typeof group === "string" && (group === key || GROUP_NAME.test(group));
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Creates a metadata decorator for controllers and routes. The compiler stores its argument
|
|
1368
|
-
* (`value`, statically evaluated or an exported reference; `true` when omitted) in
|
|
1369
|
-
* `meta[key]`, read by guards and interceptors through `ctx.route.meta` or `decorator.get(ctx)`.
|
|
1370
|
-
*
|
|
1371
|
-
* ```ts
|
|
1372
|
-
* export const Public = createRouteDecorator('public', { group: 'access', required: true })
|
|
1373
|
-
* export const Authenticated = createRouteDecorator('authenticated', { group: 'access', required: true })
|
|
1374
|
-
* export const Roles = createRouteDecorator<readonly string[]>('roles', { group: 'access', required: true })
|
|
1375
|
-
*
|
|
1376
|
-
* @Controller('/books')
|
|
1377
|
-
* @Roles(['reader']) // the decision of every route of the controller...
|
|
1378
|
-
* export class BookController {
|
|
1379
|
-
* @Get('/:id/cover')
|
|
1380
|
-
* @Public() // ...unless the route has its own decision of the group
|
|
1381
|
-
* cover() {}
|
|
1382
|
-
* }
|
|
1383
|
-
*
|
|
1384
|
-
* // in a guard
|
|
1385
|
-
* if (Public.get(ctx)) return true
|
|
1386
|
-
* const roles = Roles.get(ctx) ?? []
|
|
1387
|
-
* ```
|
|
1388
|
-
*
|
|
1389
|
-
* Decorators of one `group` are alternatives: two of them on the same class or method are
|
|
1390
|
-
* ZNIGHT005, and the route's decision replaces the controller's (without `group`, each decorator is
|
|
1391
|
-
* its own group: the route value wins over the controller value). With `required: true`, a route
|
|
1392
|
-
* without decision fails the build (ZNIGHT023).
|
|
1393
|
-
*
|
|
1394
|
-
* @param key static: unique metadata key.
|
|
1395
|
-
* @param options static: `{ group?, required? }`.
|
|
1396
|
-
*/
|
|
1397
|
-
function createRouteDecorator(key, options) {
|
|
1398
|
-
if (typeof key !== "string" || key === "") throw new TypeError(`createRouteDecorator(): the key must be a non-empty string (received ${String(key)})`);
|
|
1399
|
-
if (options !== void 0 && (options === null || typeof options !== "object")) throw new TypeError(`createRouteDecorator('${key}'): options must be an object { group?, required? }`);
|
|
1400
|
-
const group = options?.group ?? key;
|
|
1401
|
-
if (!isGroupOf(group, key)) throw new TypeError(`createRouteDecorator('${key}'): "group" must be a name made of letters, digits, _ . - starting with a letter (received ${String(group)})`);
|
|
1402
|
-
const required = options?.required ?? false;
|
|
1403
|
-
if (typeof required !== "boolean") throw new TypeError(`createRouteDecorator('${key}'): "required" must be a boolean (received ${String(required)})`);
|
|
1404
|
-
const decorator = (..._args) => classOrMethodMarker;
|
|
1405
|
-
return Object.assign(decorator, {
|
|
1406
|
-
key,
|
|
1407
|
-
group,
|
|
1408
|
-
required,
|
|
1409
|
-
get: (ctx) => ctx.route?.meta[key],
|
|
1410
|
-
meta: (...args) => Object.freeze({
|
|
1411
|
-
key,
|
|
1412
|
-
value: args.length === 0 ? true : args[0],
|
|
1413
|
-
group,
|
|
1414
|
-
required
|
|
1415
|
-
})
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
1172
|
// src/event-channel.ts
|
|
1419
1173
|
const DEFAULT_CAPACITY = 1e3;
|
|
1420
1174
|
/**
|
|
@@ -3713,6 +3467,15 @@ const HttpTokens = Object.freeze({
|
|
|
3713
3467
|
Plugin: "@znight/http#HttpPlugin"
|
|
3714
3468
|
});
|
|
3715
3469
|
// src/pipeline.ts
|
|
3470
|
+
/**
|
|
3471
|
+
* The pending upgrade of a handshake context (RFC 0003): set by {@link Pipeline.upgrade} for the
|
|
3472
|
+
* upgrade routes only, so that ordinary requests pay nothing.
|
|
3473
|
+
*/
|
|
3474
|
+
const UPGRADES = /* @__PURE__ */ new WeakMap();
|
|
3475
|
+
/** The pending upgrade of the handshake whose context is `ctx` (internal, upgrade routes). */
|
|
3476
|
+
function pendingUpgradeOf(ctx) {
|
|
3477
|
+
return UPGRADES.get(ctx);
|
|
3478
|
+
}
|
|
3716
3479
|
/** Default event stream settings. */
|
|
3717
3480
|
const DEFAULT_SSE_SETTINGS = Object.freeze({
|
|
3718
3481
|
heartbeat: 8e3,
|
|
@@ -3816,6 +3579,8 @@ var Pipeline = class {
|
|
|
3816
3579
|
#streams = /* @__PURE__ */ new Set();
|
|
3817
3580
|
/** Set while the server drains: responses ask clients to close their connection. */
|
|
3818
3581
|
draining = false;
|
|
3582
|
+
/** The upgrade routes (RFC 0003), `undefined` when no plugin declares one. */
|
|
3583
|
+
upgrades = void 0;
|
|
3819
3584
|
constructor(container, hooks, logger, options, table) {
|
|
3820
3585
|
this.#container = container;
|
|
3821
3586
|
this.#hooks = hooks;
|
|
@@ -4264,6 +4029,71 @@ var Pipeline = class {
|
|
|
4264
4029
|
return Promise.reject(failure);
|
|
4265
4030
|
}
|
|
4266
4031
|
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Serves a WebSocket handshake (RFC 0003 §8.2.2): the upgrade route of its raw path runs through
|
|
4034
|
+
* the pipeline. Resolves with the reply to write (a refusal), or `undefined` once the transport
|
|
4035
|
+
* took the connection; the request scope then stays open until the connection closes.
|
|
4036
|
+
*/
|
|
4037
|
+
upgrade(request, native) {
|
|
4038
|
+
const startTime = performance.now();
|
|
4039
|
+
try {
|
|
4040
|
+
const url = splitUrl(request.url);
|
|
4041
|
+
const upgrade = this.upgrades?.find("GET", url.path);
|
|
4042
|
+
if (upgrade === void 0) return this.#fallback(request, void 0, startTime);
|
|
4043
|
+
const plan = upgrade.plan;
|
|
4044
|
+
const raw = matchRoutePath(plan.path, url.path) ?? emptyRecord();
|
|
4045
|
+
const params = emptyRecord();
|
|
4046
|
+
let malformed;
|
|
4047
|
+
for (const name of Object.keys(raw)) {
|
|
4048
|
+
const decoded = decodePathValue(raw[name]);
|
|
4049
|
+
if (decoded === void 0) malformed ??= name;
|
|
4050
|
+
else params[name] = decoded;
|
|
4051
|
+
}
|
|
4052
|
+
const target = malformed === void 0 ? {
|
|
4053
|
+
kind: "route",
|
|
4054
|
+
plan
|
|
4055
|
+
} : {
|
|
4056
|
+
kind: "error",
|
|
4057
|
+
error: new BadRequestException(`Malformed URL parameter "${malformed}"`)
|
|
4058
|
+
};
|
|
4059
|
+
let pending;
|
|
4060
|
+
const settled = this.#serve(request, url, plan.info, params, target, startTime, plan.body, (ctx) => {
|
|
4061
|
+
pending = new PendingUpgradeState(native, () => this.#upgradeHeaders(ctx));
|
|
4062
|
+
UPGRADES.set(ctx, pending);
|
|
4063
|
+
}).then((reply) => pending?.accepted === true ? void 0 : reply);
|
|
4064
|
+
const timeoutMs = upgrade.timeoutMs;
|
|
4065
|
+
if (timeoutMs === void 0) return settled;
|
|
4066
|
+
let timer;
|
|
4067
|
+
const expired = new Promise((resolve) => {
|
|
4068
|
+
timer = setTimeout(() => {
|
|
4069
|
+
if (pending !== void 0) pending.expired = true;
|
|
4070
|
+
resolve(adapterReply(serializeError(408, {
|
|
4071
|
+
statusCode: 408,
|
|
4072
|
+
error: "Request Timeout",
|
|
4073
|
+
message: "WebSocket handshake timed out"
|
|
4074
|
+
}, "GET"), null));
|
|
4075
|
+
}, timeoutMs);
|
|
4076
|
+
});
|
|
4077
|
+
return Promise.race([settled, expired]).finally(() => clearTimeout(timer));
|
|
4078
|
+
} catch (error) {
|
|
4079
|
+
return Promise.reject(error);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
/** The headers of a `101` response: those set on the context, its cookies and the request id. */
|
|
4083
|
+
#upgradeHeaders(ctx) {
|
|
4084
|
+
const headers = { ...ctx.responseHeaders };
|
|
4085
|
+
const cookies = ctx.cookieLines;
|
|
4086
|
+
if (cookies.length > 0) {
|
|
4087
|
+
const existing = headers["set-cookie"];
|
|
4088
|
+
const raw = [];
|
|
4089
|
+
if (typeof existing === "string") raw.push(existing);
|
|
4090
|
+
else if (existing !== void 0) raw.push(...existing);
|
|
4091
|
+
headers["set-cookie"] = [...raw, ...cookies.map((cookie) => cookie.line)];
|
|
4092
|
+
}
|
|
4093
|
+
const idHeader = this.#options.requestIdHeader;
|
|
4094
|
+
if (idHeader !== void 0 && headers[idHeader] === void 0 && (this.#validIds || isHeaderValue(ctx.requestId))) headers[idHeader] = ctx.requestId;
|
|
4095
|
+
return headers;
|
|
4096
|
+
}
|
|
4267
4097
|
#fallback(request, error, startTime, reroute = true) {
|
|
4268
4098
|
const body = this.#options.body;
|
|
4269
4099
|
if (error !== void 0) {
|
|
@@ -4284,7 +4114,7 @@ var Pipeline = class {
|
|
|
4284
4114
|
let target;
|
|
4285
4115
|
if (allowed.length === 0) target = {
|
|
4286
4116
|
kind: "error",
|
|
4287
|
-
error:
|
|
4117
|
+
error: this.#unmatched(request.method, path)
|
|
4288
4118
|
};
|
|
4289
4119
|
else if (request.method === "OPTIONS") target = {
|
|
4290
4120
|
kind: "options",
|
|
@@ -4296,6 +4126,17 @@ var Pipeline = class {
|
|
|
4296
4126
|
};
|
|
4297
4127
|
return this.#serve(request, url, void 0, emptyRecord(), target, startTime, body);
|
|
4298
4128
|
}
|
|
4129
|
+
/**
|
|
4130
|
+
* The error of a request no route matched: `404`, or `426 Upgrade Required` for a plain `GET` of
|
|
4131
|
+
* a WebSocket gateway path (RFC 0003 §8.2.1).
|
|
4132
|
+
*/
|
|
4133
|
+
#unmatched(method, path) {
|
|
4134
|
+
if ((method === "GET" || method === "HEAD") && this.upgrades?.find("GET", path) !== void 0) return new HttpException(426, "This endpoint only accepts WebSocket connections", void 0, { headers: {
|
|
4135
|
+
upgrade: "websocket",
|
|
4136
|
+
connection: "upgrade"
|
|
4137
|
+
} });
|
|
4138
|
+
return new NotFoundException(`Cannot ${method} ${truncate(path)}`);
|
|
4139
|
+
}
|
|
4299
4140
|
/** A CORS preflight (with the `cors` option): answered without running any route. */
|
|
4300
4141
|
#isPreflight(request) {
|
|
4301
4142
|
return this.#options.cors !== void 0 && isPreflight(request.method, request.headers);
|
|
@@ -4309,12 +4150,13 @@ var Pipeline = class {
|
|
|
4309
4150
|
};
|
|
4310
4151
|
return this.#serve(request, url, void 0, emptyRecord(), target, startTime, this.#options.body);
|
|
4311
4152
|
}
|
|
4312
|
-
#serve(request, url, route, params, target, startTime, body) {
|
|
4153
|
+
#serve(request, url, route, params, target, startTime, body, prepare) {
|
|
4313
4154
|
this.#enter();
|
|
4314
4155
|
let x;
|
|
4315
4156
|
try {
|
|
4316
4157
|
const requestId = this.#requestId(request);
|
|
4317
4158
|
const ctx = this.#context(request, requestId, url.path, url.query, route, params, body);
|
|
4159
|
+
if (prepare !== void 0) prepare(ctx);
|
|
4318
4160
|
if (this.draining && target.kind === "route" && target.plan.definition.sse !== void 0) ctx.abort("shutdown");
|
|
4319
4161
|
x = new Exchange(request, ctx, requestId, target, startTime);
|
|
4320
4162
|
} catch (error) {
|
|
@@ -4384,6 +4226,10 @@ var Pipeline = class {
|
|
|
4384
4226
|
} else if (failed) this.#wrapperFailed(x, hookError);
|
|
4385
4227
|
const streaming = this.#send(x, outcome, recheck, run);
|
|
4386
4228
|
if (streaming !== void 0) this.#streamed(x, streaming, await streaming.tracked.done);
|
|
4229
|
+
else if (outcome === RAW) {
|
|
4230
|
+
const held = UPGRADES.get(ctx)?.held;
|
|
4231
|
+
if (held !== void 0) await held;
|
|
4232
|
+
}
|
|
4387
4233
|
}
|
|
4388
4234
|
/**
|
|
4389
4235
|
* The reply of a request whose `http:request` wrapper failed before it was handled (`error`), or
|
|
@@ -4416,7 +4262,7 @@ var Pipeline = class {
|
|
|
4416
4262
|
const event = x.event;
|
|
4417
4263
|
if (outcome === RAW) {
|
|
4418
4264
|
ctx.commit();
|
|
4419
|
-
const status = x.request.rawStatus?.() ?? 200;
|
|
4265
|
+
const status = UPGRADES.get(ctx)?.accepted === true ? 101 : x.request.rawStatus?.() ?? 200;
|
|
4420
4266
|
event.status ??= status;
|
|
4421
4267
|
x.answer({
|
|
4422
4268
|
status,
|
|
@@ -5762,6 +5608,25 @@ var HttpPlugin = class HttpPlugin {
|
|
|
5762
5608
|
injectSse(request) {
|
|
5763
5609
|
return openInjectSse((stream) => this.injectStream(stream), request);
|
|
5764
5610
|
}
|
|
5611
|
+
/**
|
|
5612
|
+
* Runs a WebSocket handshake in memory (RFC 0003, `WebSocketPlugin.connect()`): the same pipeline
|
|
5613
|
+
* as an upgrade request of the adapter (guards, hooks, the route's checks), without socket. The
|
|
5614
|
+
* transport receives `{ kind: 'memory', peer }`. Resolves with the refusal, or `undefined` once
|
|
5615
|
+
* the connection was accepted. Available after `onBootstrap`.
|
|
5616
|
+
*/
|
|
5617
|
+
async upgradeInMemory(request, native) {
|
|
5618
|
+
const runtime = this.#require("upgradeInMemory()");
|
|
5619
|
+
const normalized = await normalizeInject(withSignedCookies(request, runtime.options.cookies, "upgradeInMemory()"));
|
|
5620
|
+
const headers = normalized.headers;
|
|
5621
|
+
headers["upgrade"] ??= "websocket";
|
|
5622
|
+
headers["connection"] ??= "Upgrade";
|
|
5623
|
+
headers["sec-websocket-version"] ??= "13";
|
|
5624
|
+
headers["sec-websocket-key"] ??= btoa(String.fromCharCode(...crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))));
|
|
5625
|
+
headers["host"] ??= "localhost";
|
|
5626
|
+
const reply = await runtime.pipeline.upgrade(new MemoryUpgradeRequest(normalized.method, normalized.url, headers), native);
|
|
5627
|
+
if (reply === void 0) return void 0;
|
|
5628
|
+
return injectResponse(reply.status, reply.headers, await replyBytes(reply));
|
|
5629
|
+
}
|
|
5765
5630
|
register(ctx) {
|
|
5766
5631
|
if (this.#active) throw new ZnightError(ErrorCode.DuplicateProvider, "This HttpPlugin is already used by another application", { hints: ["Call HttpPlugin.configure() once per application"] });
|
|
5767
5632
|
this.#active = true;
|
|
@@ -5801,16 +5666,41 @@ var HttpPlugin = class HttpPlugin {
|
|
|
5801
5666
|
cookies: options.cookies,
|
|
5802
5667
|
sse: options.sse
|
|
5803
5668
|
}, table);
|
|
5804
|
-
const
|
|
5805
|
-
|
|
5669
|
+
const prefix = options.prefix === "/" ? "" : options.prefix;
|
|
5670
|
+
const pluginRoutes = collectPluginRoutes(ctx.app.plugins, this, {
|
|
5671
|
+
prefix,
|
|
5806
5672
|
container: ctx.container
|
|
5807
|
-
})
|
|
5673
|
+
});
|
|
5674
|
+
const support = options.adapter.upgrades;
|
|
5675
|
+
const upgrades = collectPluginUpgrades(ctx.app.plugins, this, {
|
|
5676
|
+
prefix,
|
|
5677
|
+
container: ctx.container,
|
|
5678
|
+
adapter: options.adapter.name,
|
|
5679
|
+
upgrades: support?.kind
|
|
5680
|
+
});
|
|
5681
|
+
const { registrations, checked } = buildRegistrations(ctx, options, raw, pluginRoutes, upgrades.routes, pipeline);
|
|
5808
5682
|
checkDecisions(checked.routes, checked.groups, options.enforcement, ctx.logger, checked.enforcers);
|
|
5809
5683
|
const adapter = options.adapter;
|
|
5810
5684
|
const routes = [];
|
|
5685
|
+
let upgradeTable;
|
|
5811
5686
|
for (const registration of registrations) {
|
|
5812
|
-
table.add(registration.method, registration.path, registration.plan);
|
|
5813
5687
|
const plan = registration.plan;
|
|
5688
|
+
if (registration.upgrade !== void 0) {
|
|
5689
|
+
upgradeTable ??= new RouteTable();
|
|
5690
|
+
upgradeTable.add("GET", registration.path, registration.upgrade);
|
|
5691
|
+
routes.push(Object.freeze({
|
|
5692
|
+
method: "GET",
|
|
5693
|
+
path: registration.path.path,
|
|
5694
|
+
controller: displayName(plan.info.controller),
|
|
5695
|
+
handler: plan.info.handler,
|
|
5696
|
+
source: registration.source,
|
|
5697
|
+
meta: plan.info.meta,
|
|
5698
|
+
guards: registration.guards,
|
|
5699
|
+
upgrade: "websocket"
|
|
5700
|
+
}));
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
table.add(registration.method, registration.path, registration.plan);
|
|
5814
5704
|
adapter.registerRoute({
|
|
5815
5705
|
method: registration.method,
|
|
5816
5706
|
path: registration.path,
|
|
@@ -5827,6 +5717,16 @@ var HttpPlugin = class HttpPlugin {
|
|
|
5827
5717
|
}));
|
|
5828
5718
|
}
|
|
5829
5719
|
adapter.setFallbackHandler((request, error) => pipeline.fallback(request, error));
|
|
5720
|
+
if (upgradeTable !== void 0) {
|
|
5721
|
+
if (support === void 0) throw new ZnightError(ErrorCode.WebSocketTransportUnavailable, `HttpPlugin: the ${adapter.name} adapter cannot serve WebSocket upgrade routes`, { hints: ["Use a built-in adapter (node, fastify, express, hono) or implement HttpAdapter.upgrades"] });
|
|
5722
|
+
const found = upgradeTable;
|
|
5723
|
+
pipeline.upgrades = found;
|
|
5724
|
+
support.enable({
|
|
5725
|
+
match: (path) => found.find("GET", path) !== void 0,
|
|
5726
|
+
handle: (request, native) => pipeline.upgrade(request, native),
|
|
5727
|
+
bunWebSocket: upgrades.bunWebSocket
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5830
5730
|
this.#runtime = {
|
|
5831
5731
|
adapter,
|
|
5832
5732
|
pipeline,
|
|
@@ -6068,7 +5968,7 @@ function httpMetadata(metadata) {
|
|
|
6068
5968
|
if (http === null || typeof http !== "object" || !Array.isArray(http.controllers)) throw new ZnightError(ErrorCode.InvalidRoute, "Invalid metadata.extensions.http: expected { controllers: [...] }");
|
|
6069
5969
|
return http;
|
|
6070
5970
|
}
|
|
6071
|
-
function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
5971
|
+
function buildRegistrations(ctx, options, raw, pluginRoutes, upgradeRoutes, pipeline) {
|
|
6072
5972
|
const { container, metadata } = ctx;
|
|
6073
5973
|
const http = httpMetadata(metadata);
|
|
6074
5974
|
const groups = RouteGroups.fromMetadata(http.groups);
|
|
@@ -6215,6 +6115,76 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
6215
6115
|
meta: () => meta
|
|
6216
6116
|
});
|
|
6217
6117
|
}
|
|
6118
|
+
for (const route of upgradeRoutes) {
|
|
6119
|
+
const label = `plugin "${route.plugin}" (${route.name})`;
|
|
6120
|
+
const where = `${route.path.path} (${route.name})`;
|
|
6121
|
+
const invalidGuard = (ref) => new ZnightError(ErrorCode.InvalidRoute, `HttpPlugin: plugin "${route.plugin}": the guards of the upgrade route ${where} must be provider tokens, classes or objects with a canActivate() method (received ${describeValue(ref)})`);
|
|
6122
|
+
const guards = [...globalGuards, ...route.guards.map((ref) => typeof ref === "string" ? compiledGuard(ref, `guard of ${where}`) : guardEntries(container, metadata, [ref], invalidGuard)[0])];
|
|
6123
|
+
const values = {};
|
|
6124
|
+
for (const entry of route.meta) {
|
|
6125
|
+
groups.declare(entry, `the plugin "${route.plugin}"`);
|
|
6126
|
+
values[entry.key] = entry.value;
|
|
6127
|
+
}
|
|
6128
|
+
const meta = Object.freeze(values);
|
|
6129
|
+
pending.push(() => checkTargetMeta(meta, groupOf, `the upgrade route ${where} of the plugin "${route.plugin}"`));
|
|
6130
|
+
const accept = route.accept;
|
|
6131
|
+
const precheck = route.precheck;
|
|
6132
|
+
const checks = { canActivate: (ctx) => {
|
|
6133
|
+
checkHandshake(ctx);
|
|
6134
|
+
if (pipeline.draining) throw new ServiceUnavailableException("The server is shutting down", void 0, { headers: { "retry-after": "1" } });
|
|
6135
|
+
if (precheck === void 0) return true;
|
|
6136
|
+
const done = precheck(ctx);
|
|
6137
|
+
return done instanceof Promise ? done.then(() => true) : true;
|
|
6138
|
+
} };
|
|
6139
|
+
const definition = Object.freeze({
|
|
6140
|
+
method: "GET",
|
|
6141
|
+
path: route.relativePath,
|
|
6142
|
+
fullPath: route.relativePath,
|
|
6143
|
+
handler: route.name,
|
|
6144
|
+
meta,
|
|
6145
|
+
guards: [],
|
|
6146
|
+
interceptors: [],
|
|
6147
|
+
params: [],
|
|
6148
|
+
rawResponse: true,
|
|
6149
|
+
invoke: async (_plugin, http) => {
|
|
6150
|
+
const upgrade = pendingUpgradeOf(http);
|
|
6151
|
+
if (upgrade === void 0) throw new Error(`The upgrade route ${where} only serves upgrade requests`);
|
|
6152
|
+
if (upgrade.expired) throw handshakeExpired();
|
|
6153
|
+
await accept(http, upgrade);
|
|
6154
|
+
if (!upgrade.accepted) throw new Error(`accept() of the upgrade route ${where} did not take the connection`);
|
|
6155
|
+
}
|
|
6156
|
+
});
|
|
6157
|
+
const instance = { instance: route.instance };
|
|
6158
|
+
const chain = [{ instance: checks }, ...guards.map((guard) => guard.resolvable)];
|
|
6159
|
+
const body = bodyPlan({}, options.bodyLimit, options.uploads);
|
|
6160
|
+
const path = route.path;
|
|
6161
|
+
declared.push({
|
|
6162
|
+
method: "GET",
|
|
6163
|
+
path,
|
|
6164
|
+
plan: (method) => ({
|
|
6165
|
+
definition,
|
|
6166
|
+
invoke: definition.invoke,
|
|
6167
|
+
path,
|
|
6168
|
+
info: Object.freeze({
|
|
6169
|
+
method,
|
|
6170
|
+
path: path.path,
|
|
6171
|
+
controller: route.owner,
|
|
6172
|
+
handler: route.name,
|
|
6173
|
+
meta
|
|
6174
|
+
}),
|
|
6175
|
+
controller: instance,
|
|
6176
|
+
guards: chain,
|
|
6177
|
+
interceptors: [],
|
|
6178
|
+
parseBody: false,
|
|
6179
|
+
body
|
|
6180
|
+
}),
|
|
6181
|
+
label,
|
|
6182
|
+
source: route.plugin,
|
|
6183
|
+
guards,
|
|
6184
|
+
meta: () => meta,
|
|
6185
|
+
upgrade: { timeoutMs: route.timeoutMs }
|
|
6186
|
+
});
|
|
6187
|
+
}
|
|
6218
6188
|
const enforcedGroups = /* @__PURE__ */ new Map();
|
|
6219
6189
|
const enforcers = /* @__PURE__ */ new Map();
|
|
6220
6190
|
const readEnforces = (cls, name, chained) => {
|
|
@@ -6247,17 +6217,31 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
6247
6217
|
...route.source === "controller" ? {} : { plugin: route.source }
|
|
6248
6218
|
};
|
|
6249
6219
|
});
|
|
6250
|
-
const registration = (route, method) =>
|
|
6251
|
-
method
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6220
|
+
const registration = (route, method) => {
|
|
6221
|
+
const plan = route.plan(method);
|
|
6222
|
+
return {
|
|
6223
|
+
method,
|
|
6224
|
+
path: route.path,
|
|
6225
|
+
plan,
|
|
6226
|
+
label: route.label,
|
|
6227
|
+
source: route.source,
|
|
6228
|
+
guards: Object.freeze(route.guards.map((guard) => guard.name)),
|
|
6229
|
+
...route.upgrade === void 0 ? {} : { upgrade: {
|
|
6230
|
+
plan,
|
|
6231
|
+
timeoutMs: route.upgrade.timeoutMs
|
|
6232
|
+
} }
|
|
6233
|
+
};
|
|
6234
|
+
};
|
|
6258
6235
|
const explicit = [];
|
|
6259
6236
|
const catchAll = [];
|
|
6260
|
-
|
|
6237
|
+
const upgradeTable = new RouteTable();
|
|
6238
|
+
const upgrades = [];
|
|
6239
|
+
for (const route of declared) if (route.upgrade !== void 0) {
|
|
6240
|
+
const candidate = registration(route, "GET");
|
|
6241
|
+
const existing = upgradeTable.add("GET", candidate.path, candidate);
|
|
6242
|
+
if (existing !== void 0) throw duplicateRoute(candidate, existing);
|
|
6243
|
+
upgrades.push(candidate);
|
|
6244
|
+
} else if (route.method === "ALL") catchAll.push(route);
|
|
6261
6245
|
else explicit.push(registration(route, route.method));
|
|
6262
6246
|
const table = new RouteTable();
|
|
6263
6247
|
const accepted = [];
|
|
@@ -6279,6 +6263,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
6279
6263
|
catchAllShapes.set(route.path.shape, first);
|
|
6280
6264
|
for (const method of CONCRETE_METHODS) add(method === "GET" ? first : registration(route, method), false);
|
|
6281
6265
|
}
|
|
6266
|
+
accepted.push(...upgrades);
|
|
6282
6267
|
const result = [...accepted];
|
|
6283
6268
|
if (options.trailingSlash !== "strict") for (const candidate of accepted) {
|
|
6284
6269
|
const path = candidate.path;
|
|
@@ -6293,7 +6278,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
6293
6278
|
wildcard: false,
|
|
6294
6279
|
shape: `${path.shape}/`
|
|
6295
6280
|
};
|
|
6296
|
-
const plan = options.trailingSlash === "redirect" ? {
|
|
6281
|
+
const plan = options.trailingSlash === "redirect" && candidate.upgrade === void 0 ? {
|
|
6297
6282
|
...candidate.plan,
|
|
6298
6283
|
path: variant,
|
|
6299
6284
|
redirect: true
|
|
@@ -6301,10 +6286,15 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
|
|
|
6301
6286
|
...candidate.plan,
|
|
6302
6287
|
path: variant
|
|
6303
6288
|
};
|
|
6289
|
+
const upgrade = candidate.upgrade === void 0 ? {} : { upgrade: {
|
|
6290
|
+
...candidate.upgrade,
|
|
6291
|
+
plan
|
|
6292
|
+
} };
|
|
6304
6293
|
result.push({
|
|
6305
6294
|
...candidate,
|
|
6306
6295
|
path: variant,
|
|
6307
|
-
plan
|
|
6296
|
+
plan,
|
|
6297
|
+
...upgrade
|
|
6308
6298
|
});
|
|
6309
6299
|
}
|
|
6310
6300
|
return {
|
|
@@ -6477,6 +6467,10 @@ function resolveRef(container, metadata, ref, what, method, invalid) {
|
|
|
6477
6467
|
}
|
|
6478
6468
|
throw invalid();
|
|
6479
6469
|
}
|
|
6470
|
+
/** A handshake completed after its `408`: refused quietly (a 4xx is not logged as a server error). */
|
|
6471
|
+
function handshakeExpired() {
|
|
6472
|
+
return new HttpException(408, "WebSocket handshake timed out");
|
|
6473
|
+
}
|
|
6480
6474
|
// src/standard-schema.ts
|
|
6481
6475
|
/** Whether `value` looks like a Standard Schema v1 (has a `~standard` object with a `validate` function). */
|
|
6482
6476
|
function isStandardSchema(value) {
|