@blixis-io/http 0.1.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +732 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Voeten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# `@blixis-io/http`
|
|
2
|
+
|
|
3
|
+
The HTTP layer built on [`@blixis-io/core`](https://www.npmjs.com/package/@blixis-io/core) — routing, controllers, request validation with [Zod](https://zod.dev), guards, RFC 9457 error responses.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @blixis-io/http @blixis-io/core @blixis-io/di
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Body, Controller, Get, Param, Post, createHttpApplication } from "@blixis-io/http";
|
|
11
|
+
import { Module } from "@blixis-io/core";
|
|
12
|
+
|
|
13
|
+
@Controller("posts")
|
|
14
|
+
class PostController {
|
|
15
|
+
constructor(private readonly posts: PostService) {}
|
|
16
|
+
|
|
17
|
+
@Get(":id")
|
|
18
|
+
get(@Param("id") id: string) {
|
|
19
|
+
return this.posts.get(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Post()
|
|
23
|
+
create(@Body(CreatePostSchema) input: CreatePostInput) {
|
|
24
|
+
return this.posts.create(input);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Module({ providers: [PostService], controllers: [PostController] })
|
|
29
|
+
class AppModule {}
|
|
30
|
+
|
|
31
|
+
const app = await createHttpApplication(AppModule);
|
|
32
|
+
await app.listen(3000);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Built on the Web-standard `Request`/`Response` — the whole request pipeline is one `(Request) => Promise<Response>` function, testable in-process with no socket needed.
|
|
36
|
+
|
|
37
|
+
Part of [Blixis Framework](https://github.com/blixis-io/framework) — full docs: [Routing & Controllers](https://blixis-io.github.io/framework/concepts/routing-controllers/) · [API reference](https://blixis-io.github.io/framework/reference/blixis-http/).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Class, Token } from "@blixis-io/di";
|
|
2
|
+
import { Application, CreateApplicationOptions, ModuleRef } from "@blixis-io/core";
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { ZodType } from "zod";
|
|
5
|
+
//#region src/decorators/api-metadata.d.ts
|
|
6
|
+
interface ApiOperationOptions {
|
|
7
|
+
summary?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Defaults to `${ControllerName}_${methodName}` when omitted — every operation needs a unique one, valid or not. */
|
|
10
|
+
operationId?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Documents one route for `@blixis-io/openapi` — entirely optional, a route without it still gets a valid (derived) operationId. */
|
|
13
|
+
export declare function ApiOperation(options: ApiOperationOptions): MethodDecorator;
|
|
14
|
+
export declare function getApiOperation(target: object, propertyKey: string | symbol): ApiOperationOptions | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Applies OpenAPI tags to a controller (class position) or a single route
|
|
17
|
+
* (method position) — same dual-shape decorator as `@UseGuards`. Class-level
|
|
18
|
+
* and method-level tags both apply, concatenated, not one replacing the
|
|
19
|
+
* other.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator;
|
|
22
|
+
export declare function getClassApiTags(target: object): string[];
|
|
23
|
+
export declare function getMethodApiTags(target: object, propertyKey: string | symbol): string[];
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/decorators/controller.d.ts
|
|
26
|
+
export declare function Controller(prefix?: string): ClassDecorator;
|
|
27
|
+
export declare function getControllerPrefix(target: object): string | undefined;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/decorators/guards.d.ts
|
|
30
|
+
interface ExecutionContext {
|
|
31
|
+
request: Request;
|
|
32
|
+
params: Readonly<Record<string, string>>;
|
|
33
|
+
}
|
|
34
|
+
interface CanActivate {
|
|
35
|
+
canActivate(context: ExecutionContext): boolean | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Applies guards to a controller (class position) or a single route
|
|
39
|
+
* (method position) — the same decorator function works in both spots,
|
|
40
|
+
* matching the shape TS's legacy decorators give it: a class decorator
|
|
41
|
+
* receives no `propertyKey`, a method decorator does.
|
|
42
|
+
*/
|
|
43
|
+
export declare function UseGuards(...guards: Class<CanActivate>[]): ClassDecorator & MethodDecorator;
|
|
44
|
+
export declare function getClassGuards(target: object): Class<CanActivate>[];
|
|
45
|
+
export declare function getMethodGuards(target: object, propertyKey: string | symbol): Class<CanActivate>[];
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/decorators/interceptors.d.ts
|
|
48
|
+
interface Interceptor {
|
|
49
|
+
intercept(context: ExecutionContext, next: () => Promise<Response>): Response | Promise<Response>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Applies interceptors to a controller (class position) or a single route
|
|
53
|
+
* (method position) — same dual-shape decorator as `@UseGuards`. Interceptors
|
|
54
|
+
* wrap param resolution + handler invocation (guards already ran and
|
|
55
|
+
* already decided the request is allowed through); class-level interceptors
|
|
56
|
+
* wrap outermost, method-level innermost, closest to the handler.
|
|
57
|
+
*/
|
|
58
|
+
export declare function UseInterceptors(...interceptors: Class<Interceptor>[]): ClassDecorator & MethodDecorator;
|
|
59
|
+
export declare function getClassInterceptors(target: object): Class<Interceptor>[];
|
|
60
|
+
export declare function getMethodInterceptors(target: object, propertyKey: string | symbol): Class<Interceptor>[];
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/decorators/params.d.ts
|
|
63
|
+
type ParamSource = {
|
|
64
|
+
kind: "body";
|
|
65
|
+
schema?: ZodType | undefined;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "query";
|
|
68
|
+
schema?: ZodType | undefined;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "param";
|
|
71
|
+
name: string;
|
|
72
|
+
schema?: ZodType | undefined;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "headers";
|
|
75
|
+
name?: string | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "req";
|
|
78
|
+
};
|
|
79
|
+
/** Parses the JSON request body, validating it with `schema` when given. */
|
|
80
|
+
export declare function Body(schema?: ZodType): ParameterDecorator;
|
|
81
|
+
/** The parsed query string as an object, validated with `schema` when given. */
|
|
82
|
+
export declare function Query(schema?: ZodType): ParameterDecorator;
|
|
83
|
+
/** One route param by name, validated with `schema` when given (otherwise the raw string). */
|
|
84
|
+
export declare function Param(name: string, schema?: ZodType): ParameterDecorator;
|
|
85
|
+
/** One request header by name, or all headers as an object when `name` is omitted. */
|
|
86
|
+
export declare function Headers(name?: string): ParameterDecorator;
|
|
87
|
+
/** The raw Web-standard `Request`. */
|
|
88
|
+
export declare function Req(): ParameterDecorator;
|
|
89
|
+
export declare function getParamSources(target: object, propertyKey: string | symbol): Map<number, ParamSource>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/types.d.ts
|
|
92
|
+
export declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
93
|
+
type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/decorators/routes.d.ts
|
|
96
|
+
interface RouteDefinition {
|
|
97
|
+
method: HttpMethod;
|
|
98
|
+
path: string;
|
|
99
|
+
propertyKey: string | symbol;
|
|
100
|
+
}
|
|
101
|
+
export declare const Get: (path?: string) => MethodDecorator;
|
|
102
|
+
export declare const Post: (path?: string) => MethodDecorator;
|
|
103
|
+
export declare const Put: (path?: string) => MethodDecorator;
|
|
104
|
+
export declare const Patch: (path?: string) => MethodDecorator;
|
|
105
|
+
export declare const Delete: (path?: string) => MethodDecorator;
|
|
106
|
+
export declare function getRoutes(target: object): RouteDefinition[];
|
|
107
|
+
/** Overrides the default 200 (or 204 for an `undefined` return) status for one route. */
|
|
108
|
+
export declare function HttpCode(code: number): MethodDecorator;
|
|
109
|
+
export declare function getHttpCode(target: object, propertyKey: string | symbol): number | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Declares the response body's shape for one route. Validated after the
|
|
112
|
+
* handler returns, before serialization — a mismatch is a server bug (the
|
|
113
|
+
* implementation drifted from its own declared contract), not a client
|
|
114
|
+
* error, so it never reaches the client as anything but a generic 500.
|
|
115
|
+
*/
|
|
116
|
+
export declare function Returns(schema: ZodType): MethodDecorator;
|
|
117
|
+
export declare function getReturnsSchema(target: object, propertyKey: string | symbol): ZodType | undefined;
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/exceptions.d.ts
|
|
120
|
+
/** Thrown from a guard or handler to short-circuit the response with a specific status. */
|
|
121
|
+
export declare class HttpException extends Error {
|
|
122
|
+
readonly status: number;
|
|
123
|
+
readonly detail: string;
|
|
124
|
+
readonly extra?: Record<string, unknown> | undefined;
|
|
125
|
+
constructor(status: number, detail: string, extra?: Record<string, unknown> | undefined);
|
|
126
|
+
}
|
|
127
|
+
export declare class BadRequestException extends HttpException {
|
|
128
|
+
constructor(detail?: string, extra?: Record<string, unknown>);
|
|
129
|
+
}
|
|
130
|
+
export declare class UnauthorizedException extends HttpException {
|
|
131
|
+
constructor(detail?: string);
|
|
132
|
+
}
|
|
133
|
+
export declare class ForbiddenException extends HttpException {
|
|
134
|
+
constructor(detail?: string);
|
|
135
|
+
}
|
|
136
|
+
export declare class NotFoundException extends HttpException {
|
|
137
|
+
constructor(detail?: string);
|
|
138
|
+
}
|
|
139
|
+
export declare class ConflictException extends HttpException {
|
|
140
|
+
constructor(detail?: string);
|
|
141
|
+
}
|
|
142
|
+
export declare class PayloadTooLargeException extends HttpException {
|
|
143
|
+
constructor(detail?: string);
|
|
144
|
+
}
|
|
145
|
+
export declare class UnsupportedMediaTypeException extends HttpException {
|
|
146
|
+
constructor(detail?: string);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/router.d.ts
|
|
150
|
+
interface RouteFound<T> {
|
|
151
|
+
kind: "found";
|
|
152
|
+
handler: T;
|
|
153
|
+
params: Record<string, string>;
|
|
154
|
+
}
|
|
155
|
+
interface RouteNotFound {
|
|
156
|
+
kind: "not-found";
|
|
157
|
+
}
|
|
158
|
+
interface RouteMethodNotAllowed {
|
|
159
|
+
kind: "method-not-allowed";
|
|
160
|
+
allowed: HttpMethod[];
|
|
161
|
+
}
|
|
162
|
+
type RouteLookupResult<T> = RouteFound<T> | RouteNotFound | RouteMethodNotAllowed;
|
|
163
|
+
/**
|
|
164
|
+
* A path router keyed on an opaque handler type `T` — this package plugs in
|
|
165
|
+
* controller method references; tests plug in plain strings.
|
|
166
|
+
*/
|
|
167
|
+
export declare class Router<T> {
|
|
168
|
+
#private;
|
|
169
|
+
add(method: HttpMethod, path: string, handler: T): void;
|
|
170
|
+
match(method: HttpMethod, path: string): RouteLookupResult<T>;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/handler.d.ts
|
|
174
|
+
interface HandlerOptions {
|
|
175
|
+
bodyLimit?: number;
|
|
176
|
+
}
|
|
177
|
+
export declare class NotAControllerError extends Error {
|
|
178
|
+
readonly name = "NotAControllerError";
|
|
179
|
+
constructor(target: {
|
|
180
|
+
name: string;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
interface RouteEntry {
|
|
184
|
+
controller: Class;
|
|
185
|
+
propertyKey: string | symbol;
|
|
186
|
+
paramSources: ReadonlyMap<number, ParamSource>;
|
|
187
|
+
httpCode?: number | undefined;
|
|
188
|
+
responseSchema?: ZodType | undefined;
|
|
189
|
+
guards: Class<CanActivate>[];
|
|
190
|
+
interceptors: Class<Interceptor>[];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Flattens every `@Controller`'s route metadata into one `Router`. Guard
|
|
194
|
+
* classes referenced via `@UseGuards` must also be registered as providers
|
|
195
|
+
* on the owning module — this doesn't instantiate them, it only records
|
|
196
|
+
* which classes `createHandler` will later ask the `Application` for.
|
|
197
|
+
*/
|
|
198
|
+
export declare function buildRouter(controllers: readonly Class[]): Router<RouteEntry>;
|
|
199
|
+
/** Builds a single `(Request) => Promise<Response>` function serving every controller's routes. */
|
|
200
|
+
export declare function createHandler(controllers: readonly Class[], application: Application, options?: HandlerOptions): (request: Request) => Promise<Response>;
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/http-application.d.ts
|
|
203
|
+
type HttpApplicationOptions = HandlerOptions & CreateApplicationOptions;
|
|
204
|
+
interface ListenHandle {
|
|
205
|
+
port: number;
|
|
206
|
+
}
|
|
207
|
+
/** The core `Application` plus a request handler, optionally bound to a real Node HTTP socket. */
|
|
208
|
+
export declare class HttpApplication {
|
|
209
|
+
#private;
|
|
210
|
+
private constructor();
|
|
211
|
+
static create(rootModule: ModuleRef, options?: HttpApplicationOptions): Promise<HttpApplication>;
|
|
212
|
+
/** Fetches an already-resolved provider directly, bypassing HTTP entirely. */
|
|
213
|
+
get<T>(token: Token<T>): T;
|
|
214
|
+
/** Every controller class in this app's module graph — what `@blixis-io/openapi` walks to build a document that stays in sync with the real running app. */
|
|
215
|
+
get controllers(): readonly Class[];
|
|
216
|
+
/** Runs a request through the handler in-process, without a socket. */
|
|
217
|
+
handle(request: Request): Promise<Response>;
|
|
218
|
+
listen(port: number, hostname?: string): Promise<ListenHandle>;
|
|
219
|
+
close(signal?: string): Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
export declare function createHttpApplication(rootModule: ModuleRef, options?: HttpApplicationOptions): Promise<HttpApplication>;
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/node-adapter.d.ts
|
|
224
|
+
/** Builds a Web-standard `Request` from a Node `IncomingMessage`, including a body stream and abort signal. */
|
|
225
|
+
export declare function toWebRequest(req: IncomingMessage, baseUrl: string): Request;
|
|
226
|
+
/** Writes a Web-standard `Response` back onto a Node `ServerResponse`. */
|
|
227
|
+
export declare function sendWebResponse(response: Response, res: ServerResponse): Promise<void>;
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/params.d.ts
|
|
230
|
+
interface ParamResolutionContext {
|
|
231
|
+
request: Request;
|
|
232
|
+
routeParams: Readonly<Record<string, string>>;
|
|
233
|
+
/** Reads and JSON-parses the body, memoized per-request by the caller. Rejects with an HttpException (400/413/415) on failure. */
|
|
234
|
+
getBody: () => Promise<unknown>;
|
|
235
|
+
}
|
|
236
|
+
/** Builds the positional argument list for a route handler from its `@Body`/`@Query`/`@Param`/`@Headers`/`@Req` metadata. */
|
|
237
|
+
export declare function resolveHandlerArgs(sources: ReadonlyMap<number, ParamSource>, context: ParamResolutionContext): Promise<unknown[]>;
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/request-context.d.ts
|
|
240
|
+
/** Thrown by `RequestContext.set()` when called outside an active request scope. */
|
|
241
|
+
export declare class RequestContextError extends Error {
|
|
242
|
+
constructor();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Per-request key/value store. Injectable as a singleton because its
|
|
246
|
+
* methods don't hold state themselves — they read/write whichever
|
|
247
|
+
* `Map` `runInRequestContext` made current for the request now being
|
|
248
|
+
* handled, via `AsyncLocalStorage`. Reading (`get`/`has`) outside a
|
|
249
|
+
* request is a legitimate "no request" answer; writing isn't, so
|
|
250
|
+
* `set()` throws instead of silently doing nothing.
|
|
251
|
+
*/
|
|
252
|
+
export declare class RequestContext {
|
|
253
|
+
get<T = unknown>(key: string): T | undefined;
|
|
254
|
+
has(key: string): boolean;
|
|
255
|
+
set(key: string, value: unknown): void;
|
|
256
|
+
}
|
|
257
|
+
/** Runs `fn` with a fresh, isolated store current for its whole async call chain — what `createHandler` wraps around each request. */
|
|
258
|
+
export declare function runInRequestContext<T>(fn: () => T): T;
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/response.d.ts
|
|
261
|
+
interface ResponseValidationIssue {
|
|
262
|
+
path: PropertyKey[];
|
|
263
|
+
message: string;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Thrown when a handler's return value doesn't match its `@Returns`
|
|
267
|
+
* schema. Deliberately not an `HttpException` — this is a server-side
|
|
268
|
+
* contract bug, not the client's fault, so it falls through to the
|
|
269
|
+
* generic 500 path instead of exposing the mismatch (and the app's
|
|
270
|
+
* internal shape) to the client.
|
|
271
|
+
*/
|
|
272
|
+
export declare class ResponseValidationError extends Error {
|
|
273
|
+
readonly issues: readonly ResponseValidationIssue[];
|
|
274
|
+
constructor(issues: readonly ResponseValidationIssue[]);
|
|
275
|
+
}
|
|
276
|
+
/** Validates a handler's return value against its `@Returns` schema, if any — returns the value (possibly parsed/coerced) unchanged when there's no schema. */
|
|
277
|
+
export declare function validateResponse(schema: ZodType | undefined, value: unknown): Promise<unknown>;
|
|
278
|
+
//#endregion
|
|
279
|
+
export type { ApiOperationOptions, CanActivate, ExecutionContext, HandlerOptions, HttpApplicationOptions, HttpMethod, Interceptor, ListenHandle, ParamResolutionContext, ParamSource, RouteDefinition, RouteFound, RouteLookupResult, RouteMethodNotAllowed, RouteNotFound };
|
|
280
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/decorators/api-metadata.ts","../src/decorators/controller.ts","../src/decorators/guards.ts","../src/decorators/interceptors.ts","../src/decorators/params.ts","../src/types.ts","../src/decorators/routes.ts","../src/exceptions.ts","../src/router.ts","../src/handler.ts","../src/http-application.ts","../src/node-adapter.ts","../src/params.ts","../src/request-context.ts","../src/response.ts"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { Injectable, defineMetadata, getMetadata } from "@blixis-io/di";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { Module, createApplication } from "@blixis-io/core";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
//#region src/decorators/api-metadata.ts
|
|
7
|
+
const API_OPERATION = Symbol("blixis:api-operation");
|
|
8
|
+
const API_TAGS = Symbol("blixis:api-tags");
|
|
9
|
+
/** Documents one route for `@blixis-io/openapi` — entirely optional, a route without it still gets a valid (derived) operationId. */
|
|
10
|
+
function ApiOperation(options) {
|
|
11
|
+
return (target, propertyKey) => {
|
|
12
|
+
defineMetadata(API_OPERATION, options, target, propertyKey);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function getApiOperation(target, propertyKey) {
|
|
16
|
+
return getMetadata(API_OPERATION, target, propertyKey);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Applies OpenAPI tags to a controller (class position) or a single route
|
|
20
|
+
* (method position) — same dual-shape decorator as `@UseGuards`. Class-level
|
|
21
|
+
* and method-level tags both apply, concatenated, not one replacing the
|
|
22
|
+
* other.
|
|
23
|
+
*/
|
|
24
|
+
function ApiTags(...tags) {
|
|
25
|
+
const decorator = (target, propertyKey) => {
|
|
26
|
+
if (propertyKey === void 0) {
|
|
27
|
+
const existing = getMetadata(API_TAGS, target) ?? [];
|
|
28
|
+
defineMetadata(API_TAGS, [...existing, ...tags], target);
|
|
29
|
+
} else {
|
|
30
|
+
const existing = getMetadata(API_TAGS, target, propertyKey) ?? [];
|
|
31
|
+
defineMetadata(API_TAGS, [...existing, ...tags], target, propertyKey);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return decorator;
|
|
35
|
+
}
|
|
36
|
+
function getClassApiTags(target) {
|
|
37
|
+
return getMetadata(API_TAGS, target) ?? [];
|
|
38
|
+
}
|
|
39
|
+
function getMethodApiTags(target, propertyKey) {
|
|
40
|
+
return getMetadata(API_TAGS, target, propertyKey) ?? [];
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/decorators/controller.ts
|
|
44
|
+
const CONTROLLER_PREFIX = Symbol("blixis:controller-prefix");
|
|
45
|
+
function stripSlashes(value) {
|
|
46
|
+
return value.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
47
|
+
}
|
|
48
|
+
function Controller(prefix = "") {
|
|
49
|
+
return (target) => {
|
|
50
|
+
defineMetadata(CONTROLLER_PREFIX, stripSlashes(prefix), target);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getControllerPrefix(target) {
|
|
54
|
+
return getMetadata(CONTROLLER_PREFIX, target);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/decorators/guards.ts
|
|
58
|
+
const GUARDS = Symbol("blixis:guards");
|
|
59
|
+
/**
|
|
60
|
+
* Applies guards to a controller (class position) or a single route
|
|
61
|
+
* (method position) — the same decorator function works in both spots,
|
|
62
|
+
* matching the shape TS's legacy decorators give it: a class decorator
|
|
63
|
+
* receives no `propertyKey`, a method decorator does.
|
|
64
|
+
*/
|
|
65
|
+
function UseGuards(...guards) {
|
|
66
|
+
const decorator = (target, propertyKey) => {
|
|
67
|
+
if (propertyKey === void 0) {
|
|
68
|
+
const existing = getMetadata(GUARDS, target) ?? [];
|
|
69
|
+
defineMetadata(GUARDS, [...existing, ...guards], target);
|
|
70
|
+
} else {
|
|
71
|
+
const existing = getMetadata(GUARDS, target, propertyKey) ?? [];
|
|
72
|
+
defineMetadata(GUARDS, [...existing, ...guards], target, propertyKey);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return decorator;
|
|
76
|
+
}
|
|
77
|
+
function getClassGuards(target) {
|
|
78
|
+
return getMetadata(GUARDS, target) ?? [];
|
|
79
|
+
}
|
|
80
|
+
function getMethodGuards(target, propertyKey) {
|
|
81
|
+
return getMetadata(GUARDS, target, propertyKey) ?? [];
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/decorators/interceptors.ts
|
|
85
|
+
const INTERCEPTORS = Symbol("blixis:interceptors");
|
|
86
|
+
/**
|
|
87
|
+
* Applies interceptors to a controller (class position) or a single route
|
|
88
|
+
* (method position) — same dual-shape decorator as `@UseGuards`. Interceptors
|
|
89
|
+
* wrap param resolution + handler invocation (guards already ran and
|
|
90
|
+
* already decided the request is allowed through); class-level interceptors
|
|
91
|
+
* wrap outermost, method-level innermost, closest to the handler.
|
|
92
|
+
*/
|
|
93
|
+
function UseInterceptors(...interceptors) {
|
|
94
|
+
const decorator = (target, propertyKey) => {
|
|
95
|
+
if (propertyKey === void 0) {
|
|
96
|
+
const existing = getMetadata(INTERCEPTORS, target) ?? [];
|
|
97
|
+
defineMetadata(INTERCEPTORS, [...existing, ...interceptors], target);
|
|
98
|
+
} else {
|
|
99
|
+
const existing = getMetadata(INTERCEPTORS, target, propertyKey) ?? [];
|
|
100
|
+
defineMetadata(INTERCEPTORS, [...existing, ...interceptors], target, propertyKey);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return decorator;
|
|
104
|
+
}
|
|
105
|
+
function getClassInterceptors(target) {
|
|
106
|
+
return getMetadata(INTERCEPTORS, target) ?? [];
|
|
107
|
+
}
|
|
108
|
+
function getMethodInterceptors(target, propertyKey) {
|
|
109
|
+
return getMetadata(INTERCEPTORS, target, propertyKey) ?? [];
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/decorators/params.ts
|
|
113
|
+
const PARAMS = Symbol("blixis:params");
|
|
114
|
+
function paramDecorator(source) {
|
|
115
|
+
return (target, propertyKey, parameterIndex) => {
|
|
116
|
+
if (propertyKey === void 0) throw new Error("@Body/@Query/@Param/@Headers/@Req can only decorate route handler method parameters, not constructor parameters.");
|
|
117
|
+
const sources = getMetadata(PARAMS, target, propertyKey) ?? /* @__PURE__ */ new Map();
|
|
118
|
+
sources.set(parameterIndex, source);
|
|
119
|
+
defineMetadata(PARAMS, sources, target, propertyKey);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Parses the JSON request body, validating it with `schema` when given. */
|
|
123
|
+
function Body(schema) {
|
|
124
|
+
return paramDecorator({
|
|
125
|
+
kind: "body",
|
|
126
|
+
schema
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** The parsed query string as an object, validated with `schema` when given. */
|
|
130
|
+
function Query(schema) {
|
|
131
|
+
return paramDecorator({
|
|
132
|
+
kind: "query",
|
|
133
|
+
schema
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** One route param by name, validated with `schema` when given (otherwise the raw string). */
|
|
137
|
+
function Param(name, schema) {
|
|
138
|
+
return paramDecorator({
|
|
139
|
+
kind: "param",
|
|
140
|
+
name,
|
|
141
|
+
schema
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** One request header by name, or all headers as an object when `name` is omitted. */
|
|
145
|
+
function Headers$1(name) {
|
|
146
|
+
return paramDecorator({
|
|
147
|
+
kind: "headers",
|
|
148
|
+
name
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** The raw Web-standard `Request`. */
|
|
152
|
+
function Req() {
|
|
153
|
+
return paramDecorator({ kind: "req" });
|
|
154
|
+
}
|
|
155
|
+
function getParamSources(target, propertyKey) {
|
|
156
|
+
return getMetadata(PARAMS, target, propertyKey) ?? /* @__PURE__ */ new Map();
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/decorators/routes.ts
|
|
160
|
+
const ROUTES = Symbol("blixis:routes");
|
|
161
|
+
const HTTP_CODE = Symbol("blixis:http-code");
|
|
162
|
+
const RETURNS = Symbol("blixis:returns");
|
|
163
|
+
function routeDecorator(method, path) {
|
|
164
|
+
return (target, propertyKey) => {
|
|
165
|
+
const ctor = target.constructor;
|
|
166
|
+
const routes = getMetadata(ROUTES, ctor) ?? [];
|
|
167
|
+
routes.push({
|
|
168
|
+
method,
|
|
169
|
+
path,
|
|
170
|
+
propertyKey
|
|
171
|
+
});
|
|
172
|
+
defineMetadata(ROUTES, routes, ctor);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const Get = (path = "") => routeDecorator("GET", path);
|
|
176
|
+
const Post = (path = "") => routeDecorator("POST", path);
|
|
177
|
+
const Put = (path = "") => routeDecorator("PUT", path);
|
|
178
|
+
const Patch = (path = "") => routeDecorator("PATCH", path);
|
|
179
|
+
const Delete = (path = "") => routeDecorator("DELETE", path);
|
|
180
|
+
function getRoutes(target) {
|
|
181
|
+
return getMetadata(ROUTES, target) ?? [];
|
|
182
|
+
}
|
|
183
|
+
/** Overrides the default 200 (or 204 for an `undefined` return) status for one route. */
|
|
184
|
+
function HttpCode(code) {
|
|
185
|
+
return (target, propertyKey) => {
|
|
186
|
+
defineMetadata(HTTP_CODE, code, target, propertyKey);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function getHttpCode(target, propertyKey) {
|
|
190
|
+
return getMetadata(HTTP_CODE, target, propertyKey);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Declares the response body's shape for one route. Validated after the
|
|
194
|
+
* handler returns, before serialization — a mismatch is a server bug (the
|
|
195
|
+
* implementation drifted from its own declared contract), not a client
|
|
196
|
+
* error, so it never reaches the client as anything but a generic 500.
|
|
197
|
+
*/
|
|
198
|
+
function Returns(schema) {
|
|
199
|
+
return (target, propertyKey) => {
|
|
200
|
+
defineMetadata(RETURNS, schema, target, propertyKey);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function getReturnsSchema(target, propertyKey) {
|
|
204
|
+
return getMetadata(RETURNS, target, propertyKey);
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/exceptions.ts
|
|
208
|
+
/** Thrown from a guard or handler to short-circuit the response with a specific status. */
|
|
209
|
+
var HttpException = class extends Error {
|
|
210
|
+
constructor(status, detail, extra) {
|
|
211
|
+
super(detail);
|
|
212
|
+
this.status = status;
|
|
213
|
+
this.detail = detail;
|
|
214
|
+
this.extra = extra;
|
|
215
|
+
this.name = "HttpException";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var BadRequestException = class extends HttpException {
|
|
219
|
+
constructor(detail = "Bad Request", extra) {
|
|
220
|
+
super(400, detail, extra);
|
|
221
|
+
this.name = "BadRequestException";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var UnauthorizedException = class extends HttpException {
|
|
225
|
+
constructor(detail = "Unauthorized") {
|
|
226
|
+
super(401, detail);
|
|
227
|
+
this.name = "UnauthorizedException";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var ForbiddenException = class extends HttpException {
|
|
231
|
+
constructor(detail = "Forbidden") {
|
|
232
|
+
super(403, detail);
|
|
233
|
+
this.name = "ForbiddenException";
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var NotFoundException = class extends HttpException {
|
|
237
|
+
constructor(detail = "Not Found") {
|
|
238
|
+
super(404, detail);
|
|
239
|
+
this.name = "NotFoundException";
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var ConflictException = class extends HttpException {
|
|
243
|
+
constructor(detail = "Conflict") {
|
|
244
|
+
super(409, detail);
|
|
245
|
+
this.name = "ConflictException";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var PayloadTooLargeException = class extends HttpException {
|
|
249
|
+
constructor(detail = "Payload Too Large") {
|
|
250
|
+
super(413, detail);
|
|
251
|
+
this.name = "PayloadTooLargeException";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var UnsupportedMediaTypeException = class extends HttpException {
|
|
255
|
+
constructor(detail = "Unsupported Media Type") {
|
|
256
|
+
super(415, detail);
|
|
257
|
+
this.name = "UnsupportedMediaTypeException";
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/params.ts
|
|
262
|
+
async function parseWithSchema(schema, value) {
|
|
263
|
+
const result = await schema.safeParseAsync(value);
|
|
264
|
+
if (!result.success) throw new BadRequestException("Validation failed", { issues: result.error.issues });
|
|
265
|
+
return result.data;
|
|
266
|
+
}
|
|
267
|
+
async function resolveOne(source, context) {
|
|
268
|
+
switch (source.kind) {
|
|
269
|
+
case "req": return context.request;
|
|
270
|
+
case "headers": return source.name ? context.request.headers.get(source.name) : Object.fromEntries(context.request.headers);
|
|
271
|
+
case "param": {
|
|
272
|
+
const raw = context.routeParams[source.name];
|
|
273
|
+
return source.schema ? parseWithSchema(source.schema, raw) : raw;
|
|
274
|
+
}
|
|
275
|
+
case "query": {
|
|
276
|
+
const raw = Object.fromEntries(new URL(context.request.url).searchParams);
|
|
277
|
+
return source.schema ? parseWithSchema(source.schema, raw) : raw;
|
|
278
|
+
}
|
|
279
|
+
case "body": {
|
|
280
|
+
const raw = await context.getBody();
|
|
281
|
+
return source.schema ? parseWithSchema(source.schema, raw) : raw;
|
|
282
|
+
}
|
|
283
|
+
/* v8 ignore start -- @preserve: exhaustiveness guard, unreachable through the public API */
|
|
284
|
+
default: throw new Error(`Unreachable: unknown param source kind ${source.kind}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Builds the positional argument list for a route handler from its `@Body`/`@Query`/`@Param`/`@Headers`/`@Req` metadata. */
|
|
288
|
+
async function resolveHandlerArgs(sources, context) {
|
|
289
|
+
if (sources.size === 0) return [];
|
|
290
|
+
const args = Array.from({ length: Math.max(...sources.keys()) + 1 });
|
|
291
|
+
await Promise.all([...sources.entries()].map(async ([index, source]) => {
|
|
292
|
+
args[index] = await resolveOne(source, context);
|
|
293
|
+
}));
|
|
294
|
+
return args;
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
|
|
298
|
+
function __decorate(decorators, target, key, desc) {
|
|
299
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
300
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
301
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
302
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/request-context.ts
|
|
306
|
+
const storage = new AsyncLocalStorage();
|
|
307
|
+
/** Thrown by `RequestContext.set()` when called outside an active request scope. */
|
|
308
|
+
var RequestContextError = class extends Error {
|
|
309
|
+
constructor() {
|
|
310
|
+
super("RequestContext.set() can only be called while handling a request.");
|
|
311
|
+
this.name = "RequestContextError";
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
let RequestContext = class RequestContext {
|
|
315
|
+
get(key) {
|
|
316
|
+
return storage.getStore()?.get(key);
|
|
317
|
+
}
|
|
318
|
+
has(key) {
|
|
319
|
+
return storage.getStore()?.has(key) ?? false;
|
|
320
|
+
}
|
|
321
|
+
set(key, value) {
|
|
322
|
+
const store = storage.getStore();
|
|
323
|
+
if (!store) throw new RequestContextError();
|
|
324
|
+
store.set(key, value);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
RequestContext = __decorate([Injectable()], RequestContext);
|
|
328
|
+
/** Runs `fn` with a fresh, isolated store current for its whole async call chain — what `createHandler` wraps around each request. */
|
|
329
|
+
function runInRequestContext(fn) {
|
|
330
|
+
return storage.run(/* @__PURE__ */ new Map(), fn);
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/response.ts
|
|
334
|
+
/**
|
|
335
|
+
* Thrown when a handler's return value doesn't match its `@Returns`
|
|
336
|
+
* schema. Deliberately not an `HttpException` — this is a server-side
|
|
337
|
+
* contract bug, not the client's fault, so it falls through to the
|
|
338
|
+
* generic 500 path instead of exposing the mismatch (and the app's
|
|
339
|
+
* internal shape) to the client.
|
|
340
|
+
*/
|
|
341
|
+
var ResponseValidationError = class extends Error {
|
|
342
|
+
constructor(issues) {
|
|
343
|
+
super(`Response failed its declared schema: ${issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
344
|
+
this.issues = issues;
|
|
345
|
+
this.name = "ResponseValidationError";
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
/** Validates a handler's return value against its `@Returns` schema, if any — returns the value (possibly parsed/coerced) unchanged when there's no schema. */
|
|
349
|
+
async function validateResponse(schema, value) {
|
|
350
|
+
if (!schema) return value;
|
|
351
|
+
const result = await schema.safeParseAsync(value);
|
|
352
|
+
if (!result.success) throw new ResponseValidationError(result.error.issues);
|
|
353
|
+
return result.data;
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/router.ts
|
|
357
|
+
var DuplicateRouteError = class extends Error {
|
|
358
|
+
constructor(method, path) {
|
|
359
|
+
super(`Duplicate route: ${method} ${path} is already registered — each method+path pair must be unique.`);
|
|
360
|
+
this.name = "DuplicateRouteError";
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function createNode() {
|
|
364
|
+
return {
|
|
365
|
+
staticChildren: /* @__PURE__ */ new Map(),
|
|
366
|
+
handlers: /* @__PURE__ */ new Map()
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function splitPath(path) {
|
|
370
|
+
return path.split("/").filter((segment) => segment.length > 0);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Depth-first with backtracking: a static/param branch that leads to a dead
|
|
374
|
+
* end (no route registered there) must not block a sibling branch — e.g.
|
|
375
|
+
* `/posts/new` (static, POST only) must not swallow `/posts/:id` (GET) when
|
|
376
|
+
* someone requests GET /posts/new.
|
|
377
|
+
*/
|
|
378
|
+
function walk(node, segments, index, params) {
|
|
379
|
+
if (index === segments.length) return node.handlers.size > 0 ? {
|
|
380
|
+
node,
|
|
381
|
+
params
|
|
382
|
+
} : void 0;
|
|
383
|
+
const segment = segments[index];
|
|
384
|
+
const staticChild = node.staticChildren.get(segment);
|
|
385
|
+
if (staticChild) {
|
|
386
|
+
const result = walk(staticChild, segments, index + 1, params);
|
|
387
|
+
if (result) return result;
|
|
388
|
+
}
|
|
389
|
+
if (node.paramChild) {
|
|
390
|
+
const { name, node: paramNode } = node.paramChild;
|
|
391
|
+
const result = walk(paramNode, segments, index + 1, {
|
|
392
|
+
...params,
|
|
393
|
+
[name]: segment
|
|
394
|
+
});
|
|
395
|
+
if (result) return result;
|
|
396
|
+
}
|
|
397
|
+
if (node.wildcardChild) {
|
|
398
|
+
const { name, node: wildcardNode } = node.wildcardChild;
|
|
399
|
+
if (wildcardNode.handlers.size > 0) return {
|
|
400
|
+
node: wildcardNode,
|
|
401
|
+
params: {
|
|
402
|
+
...params,
|
|
403
|
+
[name]: segments.slice(index).join("/")
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* A path router keyed on an opaque handler type `T` — this package plugs in
|
|
410
|
+
* controller method references; tests plug in plain strings.
|
|
411
|
+
*/
|
|
412
|
+
var Router = class {
|
|
413
|
+
#root = createNode();
|
|
414
|
+
add(method, path, handler) {
|
|
415
|
+
const segments = splitPath(path);
|
|
416
|
+
let node = this.#root;
|
|
417
|
+
for (const segment of segments) if (segment.startsWith(":")) {
|
|
418
|
+
const name = segment.slice(1);
|
|
419
|
+
node.paramChild ??= {
|
|
420
|
+
name,
|
|
421
|
+
node: createNode()
|
|
422
|
+
};
|
|
423
|
+
node = node.paramChild.node;
|
|
424
|
+
} else if (segment === "*") {
|
|
425
|
+
node.wildcardChild ??= {
|
|
426
|
+
name: "*",
|
|
427
|
+
node: createNode()
|
|
428
|
+
};
|
|
429
|
+
node = node.wildcardChild.node;
|
|
430
|
+
} else {
|
|
431
|
+
let child = node.staticChildren.get(segment);
|
|
432
|
+
if (!child) {
|
|
433
|
+
child = createNode();
|
|
434
|
+
node.staticChildren.set(segment, child);
|
|
435
|
+
}
|
|
436
|
+
node = child;
|
|
437
|
+
}
|
|
438
|
+
if (node.handlers.has(method)) throw new DuplicateRouteError(method, path);
|
|
439
|
+
node.handlers.set(method, handler);
|
|
440
|
+
}
|
|
441
|
+
match(method, path) {
|
|
442
|
+
const segments = splitPath(path);
|
|
443
|
+
const result = walk(this.#root, segments, 0, {});
|
|
444
|
+
if (!result) return { kind: "not-found" };
|
|
445
|
+
const handler = result.node.handlers.get(method);
|
|
446
|
+
if (!handler) return {
|
|
447
|
+
kind: "method-not-allowed",
|
|
448
|
+
allowed: [...result.node.handlers.keys()]
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
kind: "found",
|
|
452
|
+
handler,
|
|
453
|
+
params: result.params
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/handler.ts
|
|
459
|
+
const DEFAULT_BODY_LIMIT = 1048576;
|
|
460
|
+
var NotAControllerError = class extends Error {
|
|
461
|
+
constructor(target) {
|
|
462
|
+
super(`${target.name} is not a controller — did you forget @Controller()?`);
|
|
463
|
+
this.name = "NotAControllerError";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Flattens every `@Controller`'s route metadata into one `Router`. Guard
|
|
468
|
+
* classes referenced via `@UseGuards` must also be registered as providers
|
|
469
|
+
* on the owning module — this doesn't instantiate them, it only records
|
|
470
|
+
* which classes `createHandler` will later ask the `Application` for.
|
|
471
|
+
*/
|
|
472
|
+
function buildRouter(controllers) {
|
|
473
|
+
const router = new Router();
|
|
474
|
+
for (const controller of controllers) {
|
|
475
|
+
const prefix = getControllerPrefix(controller);
|
|
476
|
+
if (prefix === void 0) throw new NotAControllerError(controller);
|
|
477
|
+
const prototype = controller.prototype;
|
|
478
|
+
const classGuards = getClassGuards(controller);
|
|
479
|
+
const classInterceptors = getClassInterceptors(controller);
|
|
480
|
+
for (const route of getRoutes(controller)) router.add(route.method, `/${prefix}/${route.path}`, {
|
|
481
|
+
controller,
|
|
482
|
+
propertyKey: route.propertyKey,
|
|
483
|
+
paramSources: getParamSources(prototype, route.propertyKey),
|
|
484
|
+
httpCode: getHttpCode(prototype, route.propertyKey),
|
|
485
|
+
responseSchema: getReturnsSchema(prototype, route.propertyKey),
|
|
486
|
+
guards: [...classGuards, ...getMethodGuards(prototype, route.propertyKey)],
|
|
487
|
+
interceptors: [...classInterceptors, ...getMethodInterceptors(prototype, route.propertyKey)]
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return router;
|
|
491
|
+
}
|
|
492
|
+
async function readJsonBody(request, bodyLimit) {
|
|
493
|
+
if (request.body === null) return;
|
|
494
|
+
if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) throw new UnsupportedMediaTypeException();
|
|
495
|
+
const contentLength = request.headers.get("content-length");
|
|
496
|
+
if (contentLength !== null && Number(contentLength) > bodyLimit) throw new PayloadTooLargeException();
|
|
497
|
+
const text = await request.text();
|
|
498
|
+
if (new TextEncoder().encode(text).length > bodyLimit) throw new PayloadTooLargeException();
|
|
499
|
+
if (text.length === 0) return;
|
|
500
|
+
try {
|
|
501
|
+
return JSON.parse(text);
|
|
502
|
+
} catch {
|
|
503
|
+
throw new BadRequestException("Invalid JSON body");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const STATUS_TITLES = {
|
|
507
|
+
400: "Bad Request",
|
|
508
|
+
401: "Unauthorized",
|
|
509
|
+
403: "Forbidden",
|
|
510
|
+
404: "Not Found",
|
|
511
|
+
405: "Method Not Allowed",
|
|
512
|
+
409: "Conflict",
|
|
513
|
+
413: "Payload Too Large",
|
|
514
|
+
415: "Unsupported Media Type",
|
|
515
|
+
500: "Internal Server Error"
|
|
516
|
+
};
|
|
517
|
+
function problemResponse(status, detail, extra) {
|
|
518
|
+
const body = {
|
|
519
|
+
type: "about:blank",
|
|
520
|
+
title: STATUS_TITLES[status] ?? "Error",
|
|
521
|
+
status,
|
|
522
|
+
detail,
|
|
523
|
+
...extra
|
|
524
|
+
};
|
|
525
|
+
return new Response(JSON.stringify(body), {
|
|
526
|
+
status,
|
|
527
|
+
headers: { "content-type": "application/problem+json" }
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function exceptionToResponse(error) {
|
|
531
|
+
if (error instanceof HttpException) return problemResponse(error.status, error.detail, error.extra);
|
|
532
|
+
console.error(error);
|
|
533
|
+
return problemResponse(500, "An unexpected error occurred");
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* A raw `Response` or an `undefined` (204) return bypasses `responseSchema`
|
|
537
|
+
* entirely — both are deliberate escape hatches from the normal JSON path,
|
|
538
|
+
* not a value the schema was ever meant to describe.
|
|
539
|
+
*/
|
|
540
|
+
async function toResponse(value, httpCode, responseSchema) {
|
|
541
|
+
if (value instanceof Response) return value;
|
|
542
|
+
if (value === void 0) return new Response(null, { status: httpCode ?? 204 });
|
|
543
|
+
const validated = await validateResponse(responseSchema, value);
|
|
544
|
+
return new Response(JSON.stringify(validated), {
|
|
545
|
+
status: httpCode ?? 200,
|
|
546
|
+
headers: { "content-type": "application/json" }
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/** Builds a single `(Request) => Promise<Response>` function serving every controller's routes. */
|
|
550
|
+
function createHandler(controllers, application, options = {}) {
|
|
551
|
+
const router = buildRouter(controllers);
|
|
552
|
+
const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
|
|
553
|
+
return async function handle(request) {
|
|
554
|
+
const url = new URL(request.url);
|
|
555
|
+
const match = router.match(request.method, url.pathname);
|
|
556
|
+
if (match.kind === "not-found") return problemResponse(404, "No route matches this path");
|
|
557
|
+
if (match.kind === "method-not-allowed") {
|
|
558
|
+
const response = problemResponse(405, `Allowed methods: ${match.allowed.join(", ")}`);
|
|
559
|
+
response.headers.set("allow", match.allowed.join(", "));
|
|
560
|
+
return response;
|
|
561
|
+
}
|
|
562
|
+
const route = match.handler;
|
|
563
|
+
return runInRequestContext(async () => {
|
|
564
|
+
try {
|
|
565
|
+
for (const guardClass of route.guards) if (!await application.get(guardClass).canActivate({
|
|
566
|
+
request,
|
|
567
|
+
params: match.params
|
|
568
|
+
})) throw new ForbiddenException();
|
|
569
|
+
const invoke = async () => {
|
|
570
|
+
let cachedBody;
|
|
571
|
+
const args = await resolveHandlerArgs(route.paramSources, {
|
|
572
|
+
request,
|
|
573
|
+
routeParams: match.params,
|
|
574
|
+
getBody: async () => {
|
|
575
|
+
cachedBody ??= { value: await readJsonBody(request, bodyLimit) };
|
|
576
|
+
return cachedBody.value;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return toResponse(await application.get(route.controller)[route.propertyKey]?.(...args), route.httpCode, route.responseSchema);
|
|
580
|
+
};
|
|
581
|
+
return await route.interceptors.reduceRight((next, interceptorClass) => {
|
|
582
|
+
const interceptor = application.get(interceptorClass);
|
|
583
|
+
return async () => interceptor.intercept({
|
|
584
|
+
request,
|
|
585
|
+
params: match.params
|
|
586
|
+
}, next);
|
|
587
|
+
}, invoke)();
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return exceptionToResponse(error);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/node-adapter.ts
|
|
596
|
+
/** Builds a Web-standard `Request` from a Node `IncomingMessage`, including a body stream and abort signal. */
|
|
597
|
+
function toWebRequest(req, baseUrl) {
|
|
598
|
+
const method = req.method ?? "GET";
|
|
599
|
+
const url = new URL(req.url ?? "/", baseUrl);
|
|
600
|
+
const headers = new Headers();
|
|
601
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
602
|
+
if (value === void 0) continue;
|
|
603
|
+
for (const one of Array.isArray(value) ? value : [value]) headers.append(name, one);
|
|
604
|
+
}
|
|
605
|
+
const controller = new AbortController();
|
|
606
|
+
req.once("aborted", () => {
|
|
607
|
+
controller.abort();
|
|
608
|
+
});
|
|
609
|
+
const hasBody = method !== "GET" && method !== "HEAD" && (req.headers["transfer-encoding"] !== void 0 || req.headers["content-length"] !== void 0 && req.headers["content-length"] !== "0");
|
|
610
|
+
return new Request(url, {
|
|
611
|
+
method,
|
|
612
|
+
headers,
|
|
613
|
+
signal: controller.signal,
|
|
614
|
+
...hasBody ? {
|
|
615
|
+
body: Readable.toWeb(req),
|
|
616
|
+
duplex: "half"
|
|
617
|
+
} : {}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/** Writes a Web-standard `Response` back onto a Node `ServerResponse`. */
|
|
621
|
+
async function sendWebResponse(response, res) {
|
|
622
|
+
const headers = {};
|
|
623
|
+
for (const [name, value] of response.headers) (headers[name] ??= []).push(value);
|
|
624
|
+
res.writeHead(response.status, headers);
|
|
625
|
+
if (!response.body) {
|
|
626
|
+
res.end();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const body = Readable.fromWeb(response.body);
|
|
630
|
+
await new Promise((resolve, reject) => {
|
|
631
|
+
res.once("finish", resolve);
|
|
632
|
+
res.once("error", reject);
|
|
633
|
+
body.once("error", reject);
|
|
634
|
+
body.pipe(res);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/http-application.ts
|
|
639
|
+
/** Provides `RequestContext` app-wide, without the user needing to import anything — every `createHttpApplication` root gets wrapped with this. */
|
|
640
|
+
let RequestContextModule = class RequestContextModule {};
|
|
641
|
+
RequestContextModule = __decorate([Module({
|
|
642
|
+
providers: [RequestContext],
|
|
643
|
+
exports: [RequestContext],
|
|
644
|
+
global: true
|
|
645
|
+
})], RequestContextModule);
|
|
646
|
+
/** Empty static anchor for the synthetic root — its own metadata is unused, only `imports` matters. */
|
|
647
|
+
let HttpRootModule = class HttpRootModule {};
|
|
648
|
+
HttpRootModule = __decorate([Module()], HttpRootModule);
|
|
649
|
+
/** The core `Application` plus a request handler, optionally bound to a real Node HTTP socket. */
|
|
650
|
+
var HttpApplication = class HttpApplication {
|
|
651
|
+
#app;
|
|
652
|
+
#handle;
|
|
653
|
+
#server;
|
|
654
|
+
constructor(app, handle) {
|
|
655
|
+
this.#app = app;
|
|
656
|
+
this.#handle = handle;
|
|
657
|
+
}
|
|
658
|
+
static async create(rootModule, options = {}) {
|
|
659
|
+
const app = await createApplication({
|
|
660
|
+
module: HttpRootModule,
|
|
661
|
+
imports: [rootModule, RequestContextModule]
|
|
662
|
+
}, { overrides: options.overrides });
|
|
663
|
+
const handle = createHandler(app.controllers, app, options);
|
|
664
|
+
return new HttpApplication(app, handle);
|
|
665
|
+
}
|
|
666
|
+
/** Fetches an already-resolved provider directly, bypassing HTTP entirely. */
|
|
667
|
+
get(token) {
|
|
668
|
+
return this.#app.get(token);
|
|
669
|
+
}
|
|
670
|
+
/** Every controller class in this app's module graph — what `@blixis-io/openapi` walks to build a document that stays in sync with the real running app. */
|
|
671
|
+
get controllers() {
|
|
672
|
+
return this.#app.controllers;
|
|
673
|
+
}
|
|
674
|
+
/** Runs a request through the handler in-process, without a socket. */
|
|
675
|
+
handle(request) {
|
|
676
|
+
return this.#handle(request);
|
|
677
|
+
}
|
|
678
|
+
listen(port, hostname = "0.0.0.0") {
|
|
679
|
+
return new Promise((resolve, reject) => {
|
|
680
|
+
const server = createServer((req, res) => {
|
|
681
|
+
const request = toWebRequest(req, `http://${hostname}:${port}`);
|
|
682
|
+
this.#handle(request).then((response) => sendWebResponse(response, res)).catch((error) => {
|
|
683
|
+
console.error(error);
|
|
684
|
+
if (!res.headersSent) res.writeHead(500);
|
|
685
|
+
res.end();
|
|
686
|
+
});
|
|
687
|
+
/* v8 ignore stop */
|
|
688
|
+
});
|
|
689
|
+
server.once("error", reject);
|
|
690
|
+
server.listen(port, hostname, () => {
|
|
691
|
+
this.#server = server;
|
|
692
|
+
const address = server.address();
|
|
693
|
+
resolve({ port: typeof address === "object" && address !== null ? address.port : port });
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
async close(signal) {
|
|
698
|
+
if (this.#server) {
|
|
699
|
+
const server = this.#server;
|
|
700
|
+
this.#server = void 0;
|
|
701
|
+
await new Promise((resolve, reject) => {
|
|
702
|
+
server.close((error) => {
|
|
703
|
+
/* v8 ignore next 3 -- @preserve */
|
|
704
|
+
if (error) {
|
|
705
|
+
reject(error);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
resolve();
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
await this.#app.close(signal);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
async function createHttpApplication(rootModule, options = {}) {
|
|
716
|
+
return HttpApplication.create(rootModule, options);
|
|
717
|
+
}
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/types.ts
|
|
720
|
+
const HTTP_METHODS = [
|
|
721
|
+
"GET",
|
|
722
|
+
"POST",
|
|
723
|
+
"PUT",
|
|
724
|
+
"PATCH",
|
|
725
|
+
"DELETE",
|
|
726
|
+
"HEAD",
|
|
727
|
+
"OPTIONS"
|
|
728
|
+
];
|
|
729
|
+
//#endregion
|
|
730
|
+
export { ApiOperation, ApiTags, BadRequestException, Body, ConflictException, Controller, Delete, ForbiddenException, Get, HTTP_METHODS, Headers$1 as Headers, HttpApplication, HttpCode, HttpException, NotAControllerError, NotFoundException, Param, Patch, PayloadTooLargeException, Post, Put, Query, Req, RequestContext, RequestContextError, ResponseValidationError, Returns, Router, UnauthorizedException, UnsupportedMediaTypeException, UseGuards, UseInterceptors, buildRouter, createHandler, createHttpApplication, getApiOperation, getClassApiTags, getClassGuards, getClassInterceptors, getControllerPrefix, getHttpCode, getMethodApiTags, getMethodGuards, getMethodInterceptors, getParamSources, getReturnsSchema, getRoutes, resolveHandlerArgs, runInRequestContext, sendWebResponse, toWebRequest, validateResponse };
|
|
731
|
+
|
|
732
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["Headers","exhaustive"],"sources":["../src/decorators/api-metadata.ts","../src/decorators/controller.ts","../src/decorators/guards.ts","../src/decorators/interceptors.ts","../src/decorators/params.ts","../src/decorators/routes.ts","../src/exceptions.ts","../src/params.ts","../src/request-context.ts","../src/response.ts","../src/router.ts","../src/handler.ts","../src/node-adapter.ts","../src/http-application.ts","../src/types.ts"],"sourcesContent":["import { defineMetadata, getMetadata } from \"@blixis-io/di\";\n\nexport interface ApiOperationOptions {\n summary?: string;\n description?: string;\n /** Defaults to `${ControllerName}_${methodName}` when omitted — every operation needs a unique one, valid or not. */\n operationId?: string;\n}\n\nconst API_OPERATION = Symbol(\"blixis:api-operation\");\nconst API_TAGS = Symbol(\"blixis:api-tags\");\n\n/** Documents one route for `@blixis-io/openapi` — entirely optional, a route without it still gets a valid (derived) operationId. */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n defineMetadata(API_OPERATION, options, target, propertyKey);\n };\n}\n\nexport function getApiOperation(target: object, propertyKey: string | symbol): ApiOperationOptions | undefined {\n return getMetadata(API_OPERATION, target, propertyKey);\n}\n\n/**\n * Applies OpenAPI tags to a controller (class position) or a single route\n * (method position) — same dual-shape decorator as `@UseGuards`. Class-level\n * and method-level tags both apply, concatenated, not one replacing the\n * other.\n */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n const decorator = (target: object, propertyKey?: string | symbol): void => {\n if (propertyKey === undefined) {\n const existing = getMetadata<string[]>(API_TAGS, target) ?? [];\n defineMetadata(API_TAGS, [...existing, ...tags], target);\n } else {\n const existing = getMetadata<string[]>(API_TAGS, target, propertyKey) ?? [];\n defineMetadata(API_TAGS, [...existing, ...tags], target, propertyKey);\n }\n };\n return decorator;\n}\n\nexport function getClassApiTags(target: object): string[] {\n return getMetadata(API_TAGS, target) ?? [];\n}\n\nexport function getMethodApiTags(target: object, propertyKey: string | symbol): string[] {\n return getMetadata(API_TAGS, target, propertyKey) ?? [];\n}\n","import { defineMetadata, getMetadata } from \"@blixis-io/di\";\n\nconst CONTROLLER_PREFIX = Symbol(\"blixis:controller-prefix\");\n\nfunction stripSlashes(value: string): string {\n return value.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n}\n\nexport function Controller(prefix = \"\"): ClassDecorator {\n return (target) => {\n defineMetadata(CONTROLLER_PREFIX, stripSlashes(prefix), target);\n };\n}\n\nexport function getControllerPrefix(target: object): string | undefined {\n return getMetadata(CONTROLLER_PREFIX, target);\n}\n","import { defineMetadata, getMetadata, type Class } from \"@blixis-io/di\";\n\nexport interface ExecutionContext {\n request: Request;\n params: Readonly<Record<string, string>>;\n}\n\nexport interface CanActivate {\n canActivate(context: ExecutionContext): boolean | Promise<boolean>;\n}\n\nconst GUARDS = Symbol(\"blixis:guards\");\n\n/**\n * Applies guards to a controller (class position) or a single route\n * (method position) — the same decorator function works in both spots,\n * matching the shape TS's legacy decorators give it: a class decorator\n * receives no `propertyKey`, a method decorator does.\n */\nexport function UseGuards(...guards: Class<CanActivate>[]): ClassDecorator & MethodDecorator {\n const decorator = (target: object, propertyKey?: string | symbol): void => {\n if (propertyKey === undefined) {\n const existing = getMetadata<Class<CanActivate>[]>(GUARDS, target) ?? [];\n defineMetadata(GUARDS, [...existing, ...guards], target);\n } else {\n const existing = getMetadata<Class<CanActivate>[]>(GUARDS, target, propertyKey) ?? [];\n defineMetadata(GUARDS, [...existing, ...guards], target, propertyKey);\n }\n };\n return decorator;\n}\n\nexport function getClassGuards(target: object): Class<CanActivate>[] {\n return getMetadata(GUARDS, target) ?? [];\n}\n\nexport function getMethodGuards(target: object, propertyKey: string | symbol): Class<CanActivate>[] {\n return getMetadata(GUARDS, target, propertyKey) ?? [];\n}\n","import { defineMetadata, getMetadata, type Class } from \"@blixis-io/di\";\nimport type { ExecutionContext } from \"./guards.js\";\n\nexport interface Interceptor {\n intercept(context: ExecutionContext, next: () => Promise<Response>): Response | Promise<Response>;\n}\n\nconst INTERCEPTORS = Symbol(\"blixis:interceptors\");\n\n/**\n * Applies interceptors to a controller (class position) or a single route\n * (method position) — same dual-shape decorator as `@UseGuards`. Interceptors\n * wrap param resolution + handler invocation (guards already ran and\n * already decided the request is allowed through); class-level interceptors\n * wrap outermost, method-level innermost, closest to the handler.\n */\nexport function UseInterceptors(...interceptors: Class<Interceptor>[]): ClassDecorator & MethodDecorator {\n const decorator = (target: object, propertyKey?: string | symbol): void => {\n if (propertyKey === undefined) {\n const existing = getMetadata<Class<Interceptor>[]>(INTERCEPTORS, target) ?? [];\n defineMetadata(INTERCEPTORS, [...existing, ...interceptors], target);\n } else {\n const existing = getMetadata<Class<Interceptor>[]>(INTERCEPTORS, target, propertyKey) ?? [];\n defineMetadata(INTERCEPTORS, [...existing, ...interceptors], target, propertyKey);\n }\n };\n return decorator;\n}\n\nexport function getClassInterceptors(target: object): Class<Interceptor>[] {\n return getMetadata(INTERCEPTORS, target) ?? [];\n}\n\nexport function getMethodInterceptors(target: object, propertyKey: string | symbol): Class<Interceptor>[] {\n return getMetadata(INTERCEPTORS, target, propertyKey) ?? [];\n}\n","import { defineMetadata, getMetadata } from \"@blixis-io/di\";\nimport type { ZodType } from \"zod\";\n\nexport type ParamSource =\n | { kind: \"body\"; schema?: ZodType | undefined }\n | { kind: \"query\"; schema?: ZodType | undefined }\n | { kind: \"param\"; name: string; schema?: ZodType | undefined }\n | { kind: \"headers\"; name?: string | undefined }\n | { kind: \"req\" };\n\nconst PARAMS = Symbol(\"blixis:params\");\n\nfunction paramDecorator(source: ParamSource): ParameterDecorator {\n return (target, propertyKey, parameterIndex) => {\n if (propertyKey === undefined) {\n throw new Error(\"@Body/@Query/@Param/@Headers/@Req can only decorate route handler method parameters, not constructor parameters.\");\n }\n const sources =\n getMetadata<Map<number, ParamSource>>(PARAMS, target, propertyKey) ?? new Map<number, ParamSource>();\n sources.set(parameterIndex, source);\n defineMetadata(PARAMS, sources, target, propertyKey);\n };\n}\n\n/** Parses the JSON request body, validating it with `schema` when given. */\nexport function Body(schema?: ZodType): ParameterDecorator {\n return paramDecorator({ kind: \"body\", schema });\n}\n\n/** The parsed query string as an object, validated with `schema` when given. */\nexport function Query(schema?: ZodType): ParameterDecorator {\n return paramDecorator({ kind: \"query\", schema });\n}\n\n/** One route param by name, validated with `schema` when given (otherwise the raw string). */\nexport function Param(name: string, schema?: ZodType): ParameterDecorator {\n return paramDecorator({ kind: \"param\", name, schema });\n}\n\n/** One request header by name, or all headers as an object when `name` is omitted. */\nexport function Headers(name?: string): ParameterDecorator {\n return paramDecorator({ kind: \"headers\", name });\n}\n\n/** The raw Web-standard `Request`. */\nexport function Req(): ParameterDecorator {\n return paramDecorator({ kind: \"req\" });\n}\n\nexport function getParamSources(target: object, propertyKey: string | symbol): Map<number, ParamSource> {\n return getMetadata(PARAMS, target, propertyKey) ?? new Map();\n}\n","import { defineMetadata, getMetadata } from \"@blixis-io/di\";\nimport type { ZodType } from \"zod\";\nimport type { HttpMethod } from \"../types.js\";\n\nexport interface RouteDefinition {\n method: HttpMethod;\n path: string;\n propertyKey: string | symbol;\n}\n\nconst ROUTES = Symbol(\"blixis:routes\");\nconst HTTP_CODE = Symbol(\"blixis:http-code\");\nconst RETURNS = Symbol(\"blixis:returns\");\n\nfunction routeDecorator(method: HttpMethod, path: string): MethodDecorator {\n return (target, propertyKey) => {\n const ctor = target.constructor;\n const routes = getMetadata<RouteDefinition[]>(ROUTES, ctor) ?? [];\n routes.push({ method, path, propertyKey });\n defineMetadata(ROUTES, routes, ctor);\n };\n}\n\nexport const Get = (path = \"\"): MethodDecorator => routeDecorator(\"GET\", path);\nexport const Post = (path = \"\"): MethodDecorator => routeDecorator(\"POST\", path);\nexport const Put = (path = \"\"): MethodDecorator => routeDecorator(\"PUT\", path);\nexport const Patch = (path = \"\"): MethodDecorator => routeDecorator(\"PATCH\", path);\nexport const Delete = (path = \"\"): MethodDecorator => routeDecorator(\"DELETE\", path);\n\nexport function getRoutes(target: object): RouteDefinition[] {\n return getMetadata(ROUTES, target) ?? [];\n}\n\n/** Overrides the default 200 (or 204 for an `undefined` return) status for one route. */\nexport function HttpCode(code: number): MethodDecorator {\n return (target, propertyKey) => {\n defineMetadata(HTTP_CODE, code, target, propertyKey);\n };\n}\n\nexport function getHttpCode(target: object, propertyKey: string | symbol): number | undefined {\n return getMetadata(HTTP_CODE, target, propertyKey);\n}\n\n/**\n * Declares the response body's shape for one route. Validated after the\n * handler returns, before serialization — a mismatch is a server bug (the\n * implementation drifted from its own declared contract), not a client\n * error, so it never reaches the client as anything but a generic 500.\n */\nexport function Returns(schema: ZodType): MethodDecorator {\n return (target, propertyKey) => {\n defineMetadata(RETURNS, schema, target, propertyKey);\n };\n}\n\nexport function getReturnsSchema(target: object, propertyKey: string | symbol): ZodType | undefined {\n return getMetadata(RETURNS, target, propertyKey);\n}\n","/** Thrown from a guard or handler to short-circuit the response with a specific status. */\nexport class HttpException extends Error {\n constructor(\n public readonly status: number,\n public readonly detail: string,\n public readonly extra?: Record<string, unknown>,\n ) {\n super(detail);\n this.name = \"HttpException\";\n }\n}\n\nexport class BadRequestException extends HttpException {\n constructor(detail = \"Bad Request\", extra?: Record<string, unknown>) {\n super(400, detail, extra);\n this.name = \"BadRequestException\";\n }\n}\n\nexport class UnauthorizedException extends HttpException {\n constructor(detail = \"Unauthorized\") {\n super(401, detail);\n this.name = \"UnauthorizedException\";\n }\n}\n\nexport class ForbiddenException extends HttpException {\n constructor(detail = \"Forbidden\") {\n super(403, detail);\n this.name = \"ForbiddenException\";\n }\n}\n\nexport class NotFoundException extends HttpException {\n constructor(detail = \"Not Found\") {\n super(404, detail);\n this.name = \"NotFoundException\";\n }\n}\n\nexport class ConflictException extends HttpException {\n constructor(detail = \"Conflict\") {\n super(409, detail);\n this.name = \"ConflictException\";\n }\n}\n\nexport class PayloadTooLargeException extends HttpException {\n constructor(detail = \"Payload Too Large\") {\n super(413, detail);\n this.name = \"PayloadTooLargeException\";\n }\n}\n\nexport class UnsupportedMediaTypeException extends HttpException {\n constructor(detail = \"Unsupported Media Type\") {\n super(415, detail);\n this.name = \"UnsupportedMediaTypeException\";\n }\n}\n","import type { ZodType } from \"zod\";\nimport type { ParamSource } from \"./decorators/params.js\";\nimport { BadRequestException } from \"./exceptions.js\";\n\nexport interface ParamResolutionContext {\n request: Request;\n routeParams: Readonly<Record<string, string>>;\n /** Reads and JSON-parses the body, memoized per-request by the caller. Rejects with an HttpException (400/413/415) on failure. */\n getBody: () => Promise<unknown>;\n}\n\nasync function parseWithSchema(schema: ZodType, value: unknown): Promise<unknown> {\n const result = await schema.safeParseAsync(value);\n if (!result.success) {\n throw new BadRequestException(\"Validation failed\", { issues: result.error.issues });\n }\n return result.data;\n}\n\nasync function resolveOne(source: ParamSource, context: ParamResolutionContext): Promise<unknown> {\n switch (source.kind) {\n case \"req\":\n return context.request;\n case \"headers\":\n return source.name ? context.request.headers.get(source.name) : Object.fromEntries(context.request.headers);\n case \"param\": {\n const raw = context.routeParams[source.name];\n return source.schema ? parseWithSchema(source.schema, raw) : raw;\n }\n case \"query\": {\n const raw = Object.fromEntries(new URL(context.request.url).searchParams);\n return source.schema ? parseWithSchema(source.schema, raw) : raw;\n }\n case \"body\": {\n const raw = await context.getBody();\n return source.schema ? parseWithSchema(source.schema, raw) : raw;\n }\n /* v8 ignore start -- @preserve: exhaustiveness guard, unreachable through the public API */\n default: {\n const exhaustive: never = source;\n throw new Error(`Unreachable: unknown param source kind ${(exhaustive as ParamSource).kind}`);\n }\n /* v8 ignore stop */\n }\n}\n\n/** Builds the positional argument list for a route handler from its `@Body`/`@Query`/`@Param`/`@Headers`/`@Req` metadata. */\nexport async function resolveHandlerArgs(\n sources: ReadonlyMap<number, ParamSource>,\n context: ParamResolutionContext,\n): Promise<unknown[]> {\n if (sources.size === 0) {\n return [];\n }\n\n const args: unknown[] = Array.from<unknown>({ length: Math.max(...sources.keys()) + 1 });\n\n await Promise.all(\n [...sources.entries()].map(async ([index, source]) => {\n args[index] = await resolveOne(source, context);\n }),\n );\n\n return args;\n}\n","import { Injectable } from \"@blixis-io/di\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst storage = new AsyncLocalStorage<Map<string, unknown>>();\n\n/** Thrown by `RequestContext.set()` when called outside an active request scope. */\nexport class RequestContextError extends Error {\n constructor() {\n super(\"RequestContext.set() can only be called while handling a request.\");\n this.name = \"RequestContextError\";\n }\n}\n\n/**\n * Per-request key/value store. Injectable as a singleton because its\n * methods don't hold state themselves — they read/write whichever\n * `Map` `runInRequestContext` made current for the request now being\n * handled, via `AsyncLocalStorage`. Reading (`get`/`has`) outside a\n * request is a legitimate \"no request\" answer; writing isn't, so\n * `set()` throws instead of silently doing nothing.\n */\n@Injectable()\nexport class RequestContext {\n get<T = unknown>(key: string): T | undefined {\n return storage.getStore()?.get(key) as T | undefined;\n }\n\n has(key: string): boolean {\n return storage.getStore()?.has(key) ?? false;\n }\n\n set(key: string, value: unknown): void {\n const store = storage.getStore();\n if (!store) {\n throw new RequestContextError();\n }\n store.set(key, value);\n }\n}\n\n/** Runs `fn` with a fresh, isolated store current for its whole async call chain — what `createHandler` wraps around each request. */\nexport function runInRequestContext<T>(fn: () => T): T {\n return storage.run(new Map(), fn);\n}\n","import type { ZodType } from \"zod\";\n\ninterface ResponseValidationIssue {\n path: PropertyKey[];\n message: string;\n}\n\n/**\n * Thrown when a handler's return value doesn't match its `@Returns`\n * schema. Deliberately not an `HttpException` — this is a server-side\n * contract bug, not the client's fault, so it falls through to the\n * generic 500 path instead of exposing the mismatch (and the app's\n * internal shape) to the client.\n */\nexport class ResponseValidationError extends Error {\n constructor(public readonly issues: readonly ResponseValidationIssue[]) {\n super(`Response failed its declared schema: ${issues.map((issue) => `${issue.path.join(\".\")}: ${issue.message}`).join(\"; \")}`);\n this.name = \"ResponseValidationError\";\n }\n}\n\n/** Validates a handler's return value against its `@Returns` schema, if any — returns the value (possibly parsed/coerced) unchanged when there's no schema. */\nexport async function validateResponse(schema: ZodType | undefined, value: unknown): Promise<unknown> {\n if (!schema) {\n return value;\n }\n const result = await schema.safeParseAsync(value);\n if (!result.success) {\n throw new ResponseValidationError(result.error.issues);\n }\n return result.data;\n}\n","import type { HttpMethod } from \"./types.js\";\n\nexport interface RouteFound<T> {\n kind: \"found\";\n handler: T;\n params: Record<string, string>;\n}\n\nexport interface RouteNotFound {\n kind: \"not-found\";\n}\n\nexport interface RouteMethodNotAllowed {\n kind: \"method-not-allowed\";\n allowed: HttpMethod[];\n}\n\nexport type RouteLookupResult<T> = RouteFound<T> | RouteNotFound | RouteMethodNotAllowed;\n\nexport class DuplicateRouteError extends Error {\n override readonly name = \"DuplicateRouteError\";\n\n constructor(method: HttpMethod, path: string) {\n super(`Duplicate route: ${method} ${path} is already registered — each method+path pair must be unique.`);\n }\n}\n\ninterface TrieNode<T> {\n staticChildren: Map<string, TrieNode<T>>;\n paramChild?: { name: string; node: TrieNode<T> };\n wildcardChild?: { name: string; node: TrieNode<T> };\n handlers: Map<HttpMethod, T>;\n}\n\nfunction createNode<T>(): TrieNode<T> {\n return { staticChildren: new Map(), handlers: new Map() };\n}\n\nfunction splitPath(path: string): string[] {\n return path.split(\"/\").filter((segment) => segment.length > 0);\n}\n\ninterface WalkResult<T> {\n node: TrieNode<T>;\n params: Record<string, string>;\n}\n\n/**\n * Depth-first with backtracking: a static/param branch that leads to a dead\n * end (no route registered there) must not block a sibling branch — e.g.\n * `/posts/new` (static, POST only) must not swallow `/posts/:id` (GET) when\n * someone requests GET /posts/new.\n */\nfunction walk<T>(\n node: TrieNode<T>,\n segments: readonly string[],\n index: number,\n params: Record<string, string>,\n): WalkResult<T> | undefined {\n if (index === segments.length) {\n return node.handlers.size > 0 ? { node, params } : undefined;\n }\n\n const segment = segments[index] as string;\n\n const staticChild = node.staticChildren.get(segment);\n if (staticChild) {\n const result = walk(staticChild, segments, index + 1, params);\n if (result) {\n return result;\n }\n }\n\n if (node.paramChild) {\n const { name, node: paramNode } = node.paramChild;\n const result = walk(paramNode, segments, index + 1, { ...params, [name]: segment });\n if (result) {\n return result;\n }\n }\n\n if (node.wildcardChild) {\n const { name, node: wildcardNode } = node.wildcardChild;\n if (wildcardNode.handlers.size > 0) {\n return { node: wildcardNode, params: { ...params, [name]: segments.slice(index).join(\"/\") } };\n }\n }\n\n return undefined;\n}\n\n/**\n * A path router keyed on an opaque handler type `T` — this package plugs in\n * controller method references; tests plug in plain strings.\n */\nexport class Router<T> {\n readonly #root = createNode<T>();\n\n add(method: HttpMethod, path: string, handler: T): void {\n const segments = splitPath(path);\n let node = this.#root;\n\n for (const segment of segments) {\n if (segment.startsWith(\":\")) {\n const name = segment.slice(1);\n node.paramChild ??= { name, node: createNode() };\n node = node.paramChild.node;\n } else if (segment === \"*\") {\n node.wildcardChild ??= { name: \"*\", node: createNode() };\n node = node.wildcardChild.node;\n } else {\n let child = node.staticChildren.get(segment);\n if (!child) {\n child = createNode();\n node.staticChildren.set(segment, child);\n }\n node = child;\n }\n }\n\n if (node.handlers.has(method)) {\n throw new DuplicateRouteError(method, path);\n }\n node.handlers.set(method, handler);\n }\n\n match(method: HttpMethod, path: string): RouteLookupResult<T> {\n const segments = splitPath(path);\n const result = walk(this.#root, segments, 0, {});\n\n if (!result) {\n return { kind: \"not-found\" };\n }\n\n const handler = result.node.handlers.get(method);\n if (!handler) {\n return { kind: \"method-not-allowed\", allowed: [...result.node.handlers.keys()] };\n }\n\n return { kind: \"found\", handler, params: result.params };\n }\n}\n","import type { Application } from \"@blixis-io/core\";\nimport type { Class } from \"@blixis-io/di\";\nimport type { ZodType } from \"zod\";\nimport { getControllerPrefix } from \"./decorators/controller.js\";\nimport { getClassGuards, getMethodGuards, type CanActivate } from \"./decorators/guards.js\";\nimport { getClassInterceptors, getMethodInterceptors, type Interceptor } from \"./decorators/interceptors.js\";\nimport { getParamSources, type ParamSource } from \"./decorators/params.js\";\nimport { getHttpCode, getReturnsSchema, getRoutes } from \"./decorators/routes.js\";\nimport {\n BadRequestException,\n ForbiddenException,\n HttpException,\n PayloadTooLargeException,\n UnsupportedMediaTypeException,\n} from \"./exceptions.js\";\nimport { resolveHandlerArgs } from \"./params.js\";\nimport { runInRequestContext } from \"./request-context.js\";\nimport { validateResponse } from \"./response.js\";\nimport { Router } from \"./router.js\";\nimport type { HttpMethod } from \"./types.js\";\n\nconst DEFAULT_BODY_LIMIT = 1024 * 1024; // 1 MiB\n\nexport interface HandlerOptions {\n bodyLimit?: number;\n}\n\nexport class NotAControllerError extends Error {\n override readonly name = \"NotAControllerError\";\n\n constructor(target: { name: string }) {\n super(`${target.name} is not a controller — did you forget @Controller()?`);\n }\n}\n\ninterface RouteEntry {\n controller: Class;\n propertyKey: string | symbol;\n paramSources: ReadonlyMap<number, ParamSource>;\n httpCode?: number | undefined;\n responseSchema?: ZodType | undefined;\n guards: Class<CanActivate>[];\n interceptors: Class<Interceptor>[];\n}\n\n/**\n * Flattens every `@Controller`'s route metadata into one `Router`. Guard\n * classes referenced via `@UseGuards` must also be registered as providers\n * on the owning module — this doesn't instantiate them, it only records\n * which classes `createHandler` will later ask the `Application` for.\n */\nexport function buildRouter(controllers: readonly Class[]): Router<RouteEntry> {\n const router = new Router<RouteEntry>();\n\n for (const controller of controllers) {\n const prefix = getControllerPrefix(controller);\n if (prefix === undefined) {\n throw new NotAControllerError(controller);\n }\n\n const prototype = controller.prototype as object;\n const classGuards = getClassGuards(controller);\n const classInterceptors = getClassInterceptors(controller);\n\n for (const route of getRoutes(controller)) {\n router.add(route.method, `/${prefix}/${route.path}`, {\n controller,\n propertyKey: route.propertyKey,\n paramSources: getParamSources(prototype, route.propertyKey),\n httpCode: getHttpCode(prototype, route.propertyKey),\n responseSchema: getReturnsSchema(prototype, route.propertyKey),\n guards: [...classGuards, ...getMethodGuards(prototype, route.propertyKey)],\n interceptors: [...classInterceptors, ...getMethodInterceptors(prototype, route.propertyKey)],\n });\n }\n }\n\n return router;\n}\n\nasync function readJsonBody(request: Request, bodyLimit: number): Promise<unknown> {\n if (request.body === null) {\n return undefined;\n }\n\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n if (!contentType.toLowerCase().startsWith(\"application/json\")) {\n throw new UnsupportedMediaTypeException();\n }\n\n const contentLength = request.headers.get(\"content-length\");\n if (contentLength !== null && Number(contentLength) > bodyLimit) {\n throw new PayloadTooLargeException();\n }\n\n const text = await request.text();\n if (new TextEncoder().encode(text).length > bodyLimit) {\n throw new PayloadTooLargeException();\n }\n if (text.length === 0) {\n return undefined;\n }\n\n try {\n return JSON.parse(text);\n } catch {\n throw new BadRequestException(\"Invalid JSON body\");\n }\n}\n\nconst STATUS_TITLES: Record<number, string> = {\n 400: \"Bad Request\",\n 401: \"Unauthorized\",\n 403: \"Forbidden\",\n 404: \"Not Found\",\n 405: \"Method Not Allowed\",\n 409: \"Conflict\",\n 413: \"Payload Too Large\",\n 415: \"Unsupported Media Type\",\n 500: \"Internal Server Error\",\n};\n\nfunction problemResponse(status: number, detail: string, extra?: Record<string, unknown>): Response {\n const body = {\n type: \"about:blank\",\n title: STATUS_TITLES[status] ?? \"Error\",\n status,\n detail,\n ...extra,\n };\n return new Response(JSON.stringify(body), { status, headers: { \"content-type\": \"application/problem+json\" } });\n}\n\nfunction exceptionToResponse(error: unknown): Response {\n if (error instanceof HttpException) {\n return problemResponse(error.status, error.detail, error.extra);\n }\n // Internal errors are logged but never surfaced to the client.\n console.error(error);\n return problemResponse(500, \"An unexpected error occurred\");\n}\n\n/**\n * A raw `Response` or an `undefined` (204) return bypasses `responseSchema`\n * entirely — both are deliberate escape hatches from the normal JSON path,\n * not a value the schema was ever meant to describe.\n */\nasync function toResponse(value: unknown, httpCode: number | undefined, responseSchema: ZodType | undefined): Promise<Response> {\n if (value instanceof Response) {\n return value;\n }\n if (value === undefined) {\n return new Response(null, { status: httpCode ?? 204 });\n }\n const validated = await validateResponse(responseSchema, value);\n return new Response(JSON.stringify(validated), {\n status: httpCode ?? 200,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\n/** Builds a single `(Request) => Promise<Response>` function serving every controller's routes. */\nexport function createHandler(\n controllers: readonly Class[],\n application: Application,\n options: HandlerOptions = {},\n): (request: Request) => Promise<Response> {\n const router = buildRouter(controllers);\n const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;\n\n return async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const match = router.match(request.method as HttpMethod, url.pathname);\n\n if (match.kind === \"not-found\") {\n return problemResponse(404, \"No route matches this path\");\n }\n if (match.kind === \"method-not-allowed\") {\n const response = problemResponse(405, `Allowed methods: ${match.allowed.join(\", \")}`);\n response.headers.set(\"allow\", match.allowed.join(\", \"));\n return response;\n }\n\n const route = match.handler;\n\n return runInRequestContext(async () => {\n try {\n // Sequential and short-circuiting on purpose: a later guard must not\n // run once an earlier one has already denied the request.\n for (const guardClass of route.guards) {\n const guard = application.get(guardClass);\n const allowed = await guard.canActivate({ request, params: match.params });\n if (!allowed) {\n throw new ForbiddenException();\n }\n }\n\n const invoke = async (): Promise<Response> => {\n let cachedBody: { value: unknown } | undefined;\n const args = await resolveHandlerArgs(route.paramSources, {\n request,\n routeParams: match.params,\n getBody: async () => {\n cachedBody ??= { value: await readJsonBody(request, bodyLimit) };\n return cachedBody.value;\n },\n });\n\n const instance = application.get(route.controller) as Record<PropertyKey, (...args: unknown[]) => unknown>;\n const result: unknown = await instance[route.propertyKey]?.(...args);\n\n return toResponse(result, route.httpCode, route.responseSchema);\n };\n\n // Class-level interceptors wrap outermost, method-level innermost —\n // built right-to-left so the first entry ends up as the outer call.\n const pipeline = route.interceptors.reduceRight<() => Promise<Response>>((next, interceptorClass) => {\n const interceptor = application.get(interceptorClass);\n return async () => interceptor.intercept({ request, params: match.params }, next);\n }, invoke);\n\n return await pipeline();\n } catch (error) {\n return exceptionToResponse(error);\n }\n });\n };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { Readable } from \"node:stream\";\n\n/** Builds a Web-standard `Request` from a Node `IncomingMessage`, including a body stream and abort signal. */\nexport function toWebRequest(req: IncomingMessage, baseUrl: string): Request {\n const method = req.method ?? \"GET\";\n const url = new URL(req.url ?? \"/\", baseUrl);\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n if (value === undefined) {\n continue;\n }\n for (const one of Array.isArray(value) ? value : [value]) {\n headers.append(name, one);\n }\n }\n\n const controller = new AbortController();\n req.once(\"aborted\", () => {\n controller.abort();\n });\n\n // GET/HEAD can never carry a body (Fetch spec forbids it on the Request\n // we're about to construct). Otherwise, trust the standard signals for\n // \"the client is actually sending body bytes\" rather than the method —\n // a bodyless POST is common and must not turn into an open, empty stream.\n const hasBody =\n method !== \"GET\" &&\n method !== \"HEAD\" &&\n (req.headers[\"transfer-encoding\"] !== undefined ||\n (req.headers[\"content-length\"] !== undefined && req.headers[\"content-length\"] !== \"0\"));\n\n return new Request(url, {\n method,\n headers,\n signal: controller.signal,\n // `duplex: \"half\"` is required by Node's fetch implementation whenever body is a stream.\n ...(hasBody ? { body: Readable.toWeb(req), duplex: \"half\" as const } : {}),\n });\n}\n\n/** Writes a Web-standard `Response` back onto a Node `ServerResponse`. */\nexport async function sendWebResponse(response: Response, res: ServerResponse): Promise<void> {\n const headers: Record<string, string[]> = {};\n for (const [name, value] of response.headers) {\n (headers[name] ??= []).push(value);\n }\n res.writeHead(response.status, headers);\n\n if (!response.body) {\n res.end();\n return;\n }\n\n const body = Readable.fromWeb(response.body);\n await new Promise<void>((resolve, reject) => {\n res.once(\"finish\", resolve);\n res.once(\"error\", reject);\n body.once(\"error\", reject);\n body.pipe(res);\n });\n}\n","import { createApplication, Module, type Application, type CreateApplicationOptions, type ModuleRef } from \"@blixis-io/core\";\nimport type { Class, Token } from \"@blixis-io/di\";\nimport { createServer, type Server } from \"node:http\";\nimport { createHandler, type HandlerOptions } from \"./handler.js\";\nimport { sendWebResponse, toWebRequest } from \"./node-adapter.js\";\nimport { RequestContext } from \"./request-context.js\";\n\nexport type HttpApplicationOptions = HandlerOptions & CreateApplicationOptions;\n\n/** Provides `RequestContext` app-wide, without the user needing to import anything — every `createHttpApplication` root gets wrapped with this. */\n@Module({ providers: [RequestContext], exports: [RequestContext], global: true })\nclass RequestContextModule {}\n\n/** Empty static anchor for the synthetic root — its own metadata is unused, only `imports` matters. */\n@Module()\nclass HttpRootModule {}\n\nexport interface ListenHandle {\n port: number;\n}\n\n/** The core `Application` plus a request handler, optionally bound to a real Node HTTP socket. */\nexport class HttpApplication {\n readonly #app: Application;\n readonly #handle: (request: Request) => Promise<Response>;\n #server: Server | undefined;\n\n private constructor(app: Application, handle: (request: Request) => Promise<Response>) {\n this.#app = app;\n this.#handle = handle;\n }\n\n static async create(rootModule: ModuleRef, options: HttpApplicationOptions = {}): Promise<HttpApplication> {\n const wrappedRoot = { module: HttpRootModule, imports: [rootModule, RequestContextModule] };\n const app = await createApplication(wrappedRoot, { overrides: options.overrides });\n const handle = createHandler(app.controllers, app, options);\n return new HttpApplication(app, handle);\n }\n\n /** Fetches an already-resolved provider directly, bypassing HTTP entirely. */\n get<T>(token: Token<T>): T {\n return this.#app.get(token);\n }\n\n /** Every controller class in this app's module graph — what `@blixis-io/openapi` walks to build a document that stays in sync with the real running app. */\n get controllers(): readonly Class[] {\n return this.#app.controllers;\n }\n\n /** Runs a request through the handler in-process, without a socket. */\n handle(request: Request): Promise<Response> {\n return this.#handle(request);\n }\n\n listen(port: number, hostname = \"0.0.0.0\"): Promise<ListenHandle> {\n return new Promise((resolve, reject) => {\n const server = createServer((req, res) => {\n const request = toWebRequest(req, `http://${hostname}:${port}`);\n this.#handle(request)\n .then((response) => sendWebResponse(response, res))\n /* v8 ignore start -- @preserve: safety net for a write failure\n (e.g. the client disconnects mid-stream); createHandler's own\n try/catch already turns every request-handling error into a\n Response, so this only fires for socket-level failures, which\n aren't reliably reproducible without a flaky, timing-dependent\n test. */\n .catch((error: unknown) => {\n console.error(error);\n if (!res.headersSent) {\n res.writeHead(500);\n }\n res.end();\n });\n /* v8 ignore stop */\n });\n\n server.once(\"error\", reject);\n server.listen(port, hostname, () => {\n this.#server = server;\n const address = server.address();\n // `address()` only returns a string for a Unix domain socket; `listen()`\n // here always takes a TCP port, so that arm is unreachable in practice.\n /* v8 ignore next -- @preserve */\n const actualPort = typeof address === \"object\" && address !== null ? address.port : port;\n resolve({ port: actualPort });\n });\n });\n }\n\n async close(signal?: string): Promise<void> {\n if (this.#server) {\n const server = this.#server;\n // Cleared before closing, not after: makes close() idempotent (a\n // second call is a no-op instead of ERR_SERVER_NOT_RUNNING) even if\n // the close below is still in flight.\n this.#server = undefined;\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n // #server is only ever set right after a successful listen() and\n // cleared right before this close(), so ERR_SERVER_NOT_RUNNING\n // (the one realistic cause) can't occur here.\n /* v8 ignore next 3 -- @preserve */\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n });\n }\n await this.#app.close(signal);\n }\n}\n\nexport async function createHttpApplication(\n rootModule: ModuleRef,\n options: HttpApplicationOptions = {},\n): Promise<HttpApplication> {\n return HttpApplication.create(rootModule, options);\n}\n","export const HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"] as const;\n\nexport type HttpMethod = (typeof HTTP_METHODS)[number];\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blixis-io/http",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The HTTP layer built on @blixis-io/core — routing, controllers, request validation with Zod, guards, RFC 9457 error responses.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Michael Voeten <michael@voeten.online>",
|
|
7
|
+
"homepage": "https://blixis-io.github.io/framework/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/blixis-io/framework.git",
|
|
11
|
+
"directory": "packages/http"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"http",
|
|
19
|
+
"framework",
|
|
20
|
+
"rest",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"@blixis-io/source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@blixis-io/core": "0.1.0",
|
|
38
|
+
"@blixis-io/di": "0.1.0",
|
|
39
|
+
"zod": "^4.6.5"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
43
|
+
"build": "rolldown -c"
|
|
44
|
+
}
|
|
45
|
+
}
|