@fluojs/http 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +142 -0
  3. package/README.md +144 -0
  4. package/dist/adapter.d.ts +58 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +42 -0
  7. package/dist/adapters/binding.d.ts +11 -0
  8. package/dist/adapters/binding.d.ts.map +1 -0
  9. package/dist/adapters/binding.js +185 -0
  10. package/dist/adapters/dto-validation-adapter.d.ts +10 -0
  11. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -0
  12. package/dist/adapters/dto-validation-adapter.js +46 -0
  13. package/dist/client-identity.d.ts +21 -0
  14. package/dist/client-identity.d.ts.map +1 -0
  15. package/dist/client-identity.js +108 -0
  16. package/dist/context/request-context.d.ts +53 -0
  17. package/dist/context/request-context.d.ts.map +1 -0
  18. package/dist/context/request-context.js +89 -0
  19. package/dist/context/sse.d.ts +21 -0
  20. package/dist/context/sse.d.ts.map +1 -0
  21. package/dist/context/sse.js +106 -0
  22. package/dist/decorators.d.ts +188 -0
  23. package/dist/decorators.d.ts.map +1 -0
  24. package/dist/decorators.js +378 -0
  25. package/dist/dispatch/dispatch-content-negotiation.d.ts +9 -0
  26. package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -0
  27. package/dist/dispatch/dispatch-content-negotiation.js +164 -0
  28. package/dist/dispatch/dispatch-error-policy.d.ts +3 -0
  29. package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -0
  30. package/dist/dispatch/dispatch-error-policy.js +24 -0
  31. package/dist/dispatch/dispatch-handler-policy.d.ts +3 -0
  32. package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -0
  33. package/dist/dispatch/dispatch-handler-policy.js +21 -0
  34. package/dist/dispatch/dispatch-response-policy.d.ts +7 -0
  35. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -0
  36. package/dist/dispatch/dispatch-response-policy.js +45 -0
  37. package/dist/dispatch/dispatch-routing-policy.d.ts +4 -0
  38. package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -0
  39. package/dist/dispatch/dispatch-routing-policy.js +14 -0
  40. package/dist/dispatch/dispatcher.d.ts +36 -0
  41. package/dist/dispatch/dispatcher.d.ts.map +1 -0
  42. package/dist/dispatch/dispatcher.js +196 -0
  43. package/dist/errors.d.ts +23 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +41 -0
  46. package/dist/exceptions.d.ts +174 -0
  47. package/dist/exceptions.d.ts.map +1 -0
  48. package/dist/exceptions.js +222 -0
  49. package/dist/guards.d.ts +3 -0
  50. package/dist/guards.d.ts.map +1 -0
  51. package/dist/guards.js +19 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +14 -0
  55. package/dist/input-error-detail.d.ts +10 -0
  56. package/dist/input-error-detail.d.ts.map +1 -0
  57. package/dist/input-error-detail.js +8 -0
  58. package/dist/interceptors.d.ts +3 -0
  59. package/dist/interceptors.d.ts.map +1 -0
  60. package/dist/interceptors.js +22 -0
  61. package/dist/internal.d.ts +3 -0
  62. package/dist/internal.d.ts.map +1 -0
  63. package/dist/internal.js +2 -0
  64. package/dist/mapping.d.ts +7 -0
  65. package/dist/mapping.d.ts.map +1 -0
  66. package/dist/mapping.js +244 -0
  67. package/dist/middleware/correlation.d.ts +3 -0
  68. package/dist/middleware/correlation.d.ts.map +1 -0
  69. package/dist/middleware/correlation.js +19 -0
  70. package/dist/middleware/cors.d.ts +11 -0
  71. package/dist/middleware/cors.d.ts.map +1 -0
  72. package/dist/middleware/cors.js +57 -0
  73. package/dist/middleware/middleware.d.ts +8 -0
  74. package/dist/middleware/middleware.d.ts.map +1 -0
  75. package/dist/middleware/middleware.js +64 -0
  76. package/dist/middleware/rate-limit.d.ts +39 -0
  77. package/dist/middleware/rate-limit.d.ts.map +1 -0
  78. package/dist/middleware/rate-limit.js +106 -0
  79. package/dist/middleware/security-headers.d.ts +12 -0
  80. package/dist/middleware/security-headers.d.ts.map +1 -0
  81. package/dist/middleware/security-headers.js +47 -0
  82. package/dist/route-path.d.ts +15 -0
  83. package/dist/route-path.d.ts.map +1 -0
  84. package/dist/route-path.js +69 -0
  85. package/dist/types.d.ts +274 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +114 -0
  88. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
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.ko.md ADDED
@@ -0,0 +1,142 @@
1
+ # @fluojs/http
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ 라우트 메타데이터를 DTO 바인딩, 검증, 가드, 인터셉터, 응답 작성으로 이어지는 요청 파이프라인으로 바꾸는 HTTP 실행 레이어입니다.
6
+
7
+ ## 목차
8
+
9
+ - [설치](#설치)
10
+ - [사용 시점](#사용-시점)
11
+ - [빠른 시작](#빠른-시작)
12
+ - [주요 패턴](#주요-패턴)
13
+ - [공개 API](#공개-api)
14
+ - [관련 패키지](#관련-패키지)
15
+ - [예제 소스](#예제-소스)
16
+
17
+ ## 설치
18
+
19
+ ```bash
20
+ npm install @fluojs/http
21
+ ```
22
+
23
+ ## 사용 시점
24
+
25
+ - `@Controller`, `@Get`, `@Post` 같은 데코레이터로 REST 스타일 엔드포인트를 선언할 때
26
+ - `@FromBody`, `@FromPath`, `@FromQuery`로 요청 데이터를 DTO에 바인딩할 때
27
+ - 가드, 인터셉터, 미들웨어를 예측 가능한 요청 라이프사이클에 얹고 싶을 때
28
+ - 현재 요청을 `RequestContext`로 깊은 호출 스택에서 접근하고 싶을 때
29
+
30
+ ## 빠른 시작
31
+
32
+ ```ts
33
+ import { Controller, FromBody, FromPath, Get, Post, RequestDto } from '@fluojs/http';
34
+ import { IsString, MinLength } from '@fluojs/validation';
35
+
36
+ class CreateUserDto {
37
+ @FromBody()
38
+ @IsString()
39
+ @MinLength(3)
40
+ name!: string;
41
+ }
42
+
43
+ @Controller('/users')
44
+ export class UserController {
45
+ @Post('/')
46
+ @RequestDto(CreateUserDto)
47
+ create(input: CreateUserDto) {
48
+ return { id: '1', name: input.name };
49
+ }
50
+
51
+ @Get('/:id')
52
+ getById(@FromPath('id') id: string) {
53
+ return { id, name: 'John Doe' };
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### 라우트 경로 계약
59
+
60
+ `@Controller()`, `@Get()`, `@Post()` 같은 HTTP 라우트 데코레이터는 다음만 허용합니다.
61
+
62
+ - `/users`, `/healthz` 같은 literal 세그먼트
63
+ - `/:id`, `/users/:userId/posts/:postId` 같은 full-segment path param
64
+
65
+ 트레일링 슬래시와 중복 슬래시는 라우트 매핑 단계에서 정규화되므로 `//users///:id/`는 `/users/:id`로 해석됩니다.
66
+
67
+ 라우트 데코레이터는 `*`, `?`, `/(.*)`, `user-:id`, `:id.json` 같은 wildcard, regex 유사 문법, mixed segment를 지원하지 않습니다. 와일드카드 매칭은 계속 `forRoutes('/users/*')` 같은 미들웨어 설정에서만 지원됩니다.
68
+
69
+ ## 주요 패턴
70
+
71
+ ### 가드와 인터셉터
72
+
73
+ ```ts
74
+ import { Controller, Get, UseGuards, UseInterceptors } from '@fluojs/http';
75
+
76
+ @Controller('/admin')
77
+ @UseGuards(AdminGuard)
78
+ @UseInterceptors(LoggingInterceptor)
79
+ class AdminController {
80
+ @Get('/')
81
+ dashboard() {
82
+ return { data: 'secret' };
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### 비동기 요청 컨텍스트
88
+
89
+ ```ts
90
+ import { getCurrentRequestContext } from '@fluojs/http';
91
+
92
+ function someDeepHelper() {
93
+ const ctx = getCurrentRequestContext();
94
+ console.log(ctx?.requestId);
95
+ }
96
+ ```
97
+
98
+ ### 프록시 뒤의 속도 제한
99
+
100
+ `createRateLimitMiddleware(...)`는 기본적으로 raw socket `remoteAddress`만으로 클라이언트 식별자를 해석합니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰하려면 해당 헤더를 신뢰 가능한 프록시가 덮어쓰는 환경에서만 `trustProxyHeaders: true`를 명시적으로 켜세요. 어댑터가 신뢰 가능한 프록시 체인도 raw socket 식별자도 제공하지 않는다면 공유 fallback 버킷에 의존하지 말고 명시적인 `keyResolver`를 설정하세요.
101
+
102
+ ### 서버 전송 이벤트
103
+
104
+ ```ts
105
+ import { Get, SseResponse, type RequestContext } from '@fluojs/http';
106
+
107
+ @Get('/events')
108
+ stream(_input: undefined, ctx: RequestContext) {
109
+ const sse = new SseResponse(ctx);
110
+ sse.send({ message: 'hello' });
111
+ return sse;
112
+ }
113
+ ```
114
+
115
+ ## 공개 API
116
+
117
+ - **라우팅 데코레이터**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`
118
+ - **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`
119
+ - **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`
120
+ - **핵심 런타임 타입**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
121
+ - **예외**: `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `InternalServerErrorException`, `PayloadTooLargeException`
122
+ - **헬퍼**: `createHandlerMapping`, `createDispatcher`, `createCorsMiddleware`, `createRateLimitMiddleware`, `getCurrentRequestContext`
123
+
124
+ ## 내부 서브경로 (`@fluojs/http/internal`)
125
+
126
+ `./internal` 서브경로는 플랫폼 어댑터와 핵심 런타임에서 사용하는 저수준 유틸리티만 내보냅니다. 이들은 변경될 수 있으며 일반적인 애플리케이션 코드에서 사용해서는 안 됩니다.
127
+
128
+ - `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
129
+ - `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
130
+
131
+ ## 관련 패키지
132
+
133
+ - `@fluojs/core`: 컨트롤러, 라우트, DTO 메타데이터를 저장합니다.
134
+ - `@fluojs/validation`: HTTP 바인딩 이후 DTO를 검증합니다.
135
+ - `@fluojs/runtime`: 부트스트랩 중 디스패처를 조립합니다.
136
+ - `@fluojs/passport`: 같은 가드 체인 안에서 인증을 연결합니다.
137
+
138
+ ## 예제 소스
139
+
140
+ - `examples/realworld-api/src/users/create-user.dto.ts`
141
+ - `examples/auth-jwt-passport/src/auth/auth.controller.ts`
142
+ - `packages/http/src/dispatch/dispatcher.test.ts`
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @fluojs/http
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ The HTTP execution layer that turns route metadata into a request pipeline with binding, validation, guards, interceptors, and response writing.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Public API](#public-api)
14
+ - [Related Packages](#related-packages)
15
+ - [Example Sources](#example-sources)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @fluojs/http
21
+ ```
22
+
23
+ ## When to Use
24
+
25
+ Use this package when you need to:
26
+
27
+ - define REST-style controllers with decorators such as `@Controller`, `@Get`, and `@Post`
28
+ - bind request data into DTOs with `@FromBody`, `@FromPath`, `@FromQuery`, and related decorators
29
+ - run guards, interceptors, and middleware in a predictable request lifecycle
30
+ - access the active request through `RequestContext` without passing it through every function
31
+
32
+ ## Quick Start
33
+
34
+ ```ts
35
+ import { Controller, FromBody, FromPath, Get, Post, RequestDto } from '@fluojs/http';
36
+ import { IsString, MinLength } from '@fluojs/validation';
37
+
38
+ class CreateUserDto {
39
+ @FromBody()
40
+ @IsString()
41
+ @MinLength(3)
42
+ name!: string;
43
+ }
44
+
45
+ @Controller('/users')
46
+ export class UserController {
47
+ @Post('/')
48
+ @RequestDto(CreateUserDto)
49
+ create(input: CreateUserDto) {
50
+ return { id: '1', name: input.name };
51
+ }
52
+
53
+ @Get('/:id')
54
+ getById(@FromPath('id') id: string) {
55
+ return { id, name: 'John Doe' };
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Route path contract
61
+
62
+ HTTP route decorators such as `@Controller()`, `@Get()`, and `@Post()` accept only:
63
+
64
+ - literal path segments like `/users` or `/healthz`
65
+ - full-segment path params like `/:id` or `/users/:userId/posts/:postId`
66
+
67
+ Trailing slashes and duplicate slashes are normalized during route mapping, so `//users///:id/` resolves to `/users/:id`.
68
+
69
+ Route decorators do **not** support wildcard, regex-like, or mixed-segment syntax such as `*`, `?`, `/(.*)`, `user-:id`, or `:id.json`. Wildcard matching remains middleware-only via `forRoutes('/users/*')`.
70
+
71
+ ## Common Patterns
72
+
73
+ ### Guards and interceptors
74
+
75
+ ```ts
76
+ import { Controller, Get, UseGuards, UseInterceptors } from '@fluojs/http';
77
+
78
+ @Controller('/admin')
79
+ @UseGuards(AdminGuard)
80
+ @UseInterceptors(LoggingInterceptor)
81
+ class AdminController {
82
+ @Get('/')
83
+ dashboard() {
84
+ return { data: 'secret' };
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Async request context
90
+
91
+ ```ts
92
+ import { getCurrentRequestContext } from '@fluojs/http';
93
+
94
+ function someDeepHelper() {
95
+ const ctx = getCurrentRequestContext();
96
+ console.log(ctx?.requestId);
97
+ }
98
+ ```
99
+
100
+ ### Rate limiting behind proxies
101
+
102
+ `createRateLimitMiddleware(...)` resolves client identity from the raw socket `remoteAddress` by default. To trust `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, opt in with `trustProxyHeaders: true` only when your adapter sits behind a trusted proxy that overwrites those headers. If your adapter exposes neither a trusted proxy chain nor a raw socket identity, provide an explicit `keyResolver`.
103
+
104
+ ### Server-sent events
105
+
106
+ ```ts
107
+ import { Get, SseResponse, type RequestContext } from '@fluojs/http';
108
+
109
+ @Get('/events')
110
+ stream(_input: undefined, ctx: RequestContext) {
111
+ const sse = new SseResponse(ctx);
112
+ sse.send({ message: 'hello' });
113
+ return sse;
114
+ }
115
+ ```
116
+
117
+ ## Public API
118
+
119
+ - **Routing decorators**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`
120
+ - **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`
121
+ - **Execution decorators**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`
122
+ - **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
123
+ - **Exceptions**: `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `InternalServerErrorException`, `PayloadTooLargeException`
124
+ - **Helpers**: `createHandlerMapping`, `createDispatcher`, `createCorsMiddleware`, `createRateLimitMiddleware`, `getCurrentRequestContext`
125
+
126
+ ## Internal Subpath (`@fluojs/http/internal`)
127
+
128
+ The `./internal` subpath exports only the low-level utilities used by platform adapters and the core runtime. These are subject to change and should not be used in typical application code.
129
+
130
+ - `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
131
+ - `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
132
+
133
+ ## Related Packages
134
+
135
+ - `@fluojs/core`: stores controller, route, and DTO metadata
136
+ - `@fluojs/validation`: validates DTOs after HTTP binding
137
+ - `@fluojs/runtime`: assembles the dispatcher during application bootstrap
138
+ - `@fluojs/passport`: plugs auth guards into the same HTTP guard chain
139
+
140
+ ## Example Sources
141
+
142
+ - `examples/realworld-api/src/users/create-user.dto.ts`
143
+ - `examples/auth-jwt-passport/src/auth/auth.controller.ts`
144
+ - `packages/http/src/dispatch/dispatcher.test.ts`
@@ -0,0 +1,58 @@
1
+ import type { MaybePromise } from '@fluojs/core';
2
+ import type { Dispatcher } from './types.js';
3
+ export interface ServerBackedHttpAdapterRealtimeCapability {
4
+ kind: 'server-backed';
5
+ server: unknown;
6
+ }
7
+ export interface UnsupportedHttpAdapterRealtimeCapability {
8
+ kind: 'unsupported';
9
+ mode: 'no-op';
10
+ reason: string;
11
+ }
12
+ export interface FetchStyleHttpAdapterRealtimeCapability {
13
+ contract: 'raw-websocket-expansion';
14
+ kind: 'fetch-style';
15
+ mode: 'request-upgrade';
16
+ reason: string;
17
+ support: 'contract-only' | 'supported';
18
+ version: 1;
19
+ }
20
+ export type HttpAdapterRealtimeCapability = ServerBackedHttpAdapterRealtimeCapability | FetchStyleHttpAdapterRealtimeCapability | UnsupportedHttpAdapterRealtimeCapability;
21
+ export declare function createServerBackedHttpAdapterRealtimeCapability(server: unknown): ServerBackedHttpAdapterRealtimeCapability;
22
+ export declare function createUnsupportedHttpAdapterRealtimeCapability(reason: string): UnsupportedHttpAdapterRealtimeCapability;
23
+ export declare function createFetchStyleHttpAdapterRealtimeCapability(reason: string, options?: {
24
+ support?: FetchStyleHttpAdapterRealtimeCapability['support'];
25
+ }): FetchStyleHttpAdapterRealtimeCapability;
26
+ /**
27
+ * Minimal HTTP adapter contract that binds the application lifecycle to a transport implementation.
28
+ */
29
+ export interface HttpApplicationAdapter {
30
+ /**
31
+ * Returns the underlying transport server object when the adapter exposes one.
32
+ *
33
+ * @returns The transport-native server instance, or `undefined` when the adapter does not expose it.
34
+ */
35
+ getServer?(): unknown;
36
+ getRealtimeCapability?(): HttpAdapterRealtimeCapability;
37
+ /**
38
+ * Starts the adapter and binds request dispatching to the framework dispatcher.
39
+ *
40
+ * @param dispatcher Dispatcher created by `@fluojs/http` that executes the request pipeline.
41
+ * @returns A promise that resolves when the adapter is ready to accept requests.
42
+ */
43
+ listen(dispatcher: Dispatcher): MaybePromise<void>;
44
+ /**
45
+ * Stops the adapter and releases transport resources.
46
+ *
47
+ * @param signal Optional shutdown reason propagated by runtime lifecycle hooks.
48
+ * @returns A promise that resolves after transport shutdown is complete.
49
+ */
50
+ close(signal?: string): MaybePromise<void>;
51
+ }
52
+ /**
53
+ * Creates a no-op adapter that preserves lifecycle behavior without binding a real HTTP server.
54
+ *
55
+ * @returns A lifecycle-compatible adapter whose `listen()` and `close()` methods resolve immediately.
56
+ */
57
+ export declare function createNoopHttpApplicationAdapter(): HttpApplicationAdapter;
58
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,yCAAyC;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,wCAAwC;IACvD,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uCAAuC;IACtD,QAAQ,EAAE,yBAAyB,CAAC;IACpC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,eAAe,GAAG,WAAW,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,6BAA6B,GACrC,yCAAyC,GACzC,uCAAuC,GACvC,wCAAwC,CAAC;AAE7C,wBAAgB,+CAA+C,CAC7D,MAAM,EAAE,OAAO,GACd,yCAAyC,CAK3C;AAED,wBAAgB,8CAA8C,CAC5D,MAAM,EAAE,MAAM,GACb,wCAAwC,CAM1C;AAED,wBAAgB,6CAA6C,CAC3D,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,uCAAuC,CAAC,SAAS,CAAC,CAAC;CACzD,GACL,uCAAuC,CASzC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC;IAEtB,qBAAqB,CAAC,IAAI,6BAA6B,CAAC;IAExD;;;;;OAKG;IACH,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEnD;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,IAAI,sBAAsB,CAUzE"}
@@ -0,0 +1,42 @@
1
+ export function createServerBackedHttpAdapterRealtimeCapability(server) {
2
+ return {
3
+ kind: 'server-backed',
4
+ server
5
+ };
6
+ }
7
+ export function createUnsupportedHttpAdapterRealtimeCapability(reason) {
8
+ return {
9
+ kind: 'unsupported',
10
+ mode: 'no-op',
11
+ reason
12
+ };
13
+ }
14
+ export function createFetchStyleHttpAdapterRealtimeCapability(reason, options = {}) {
15
+ return {
16
+ contract: 'raw-websocket-expansion',
17
+ kind: 'fetch-style',
18
+ mode: 'request-upgrade',
19
+ reason,
20
+ support: options.support ?? 'contract-only',
21
+ version: 1
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Minimal HTTP adapter contract that binds the application lifecycle to a transport implementation.
27
+ */
28
+
29
+ /**
30
+ * Creates a no-op adapter that preserves lifecycle behavior without binding a real HTTP server.
31
+ *
32
+ * @returns A lifecycle-compatible adapter whose `listen()` and `close()` methods resolve immediately.
33
+ */
34
+ export function createNoopHttpApplicationAdapter() {
35
+ return {
36
+ async close() {},
37
+ getRealtimeCapability() {
38
+ return createUnsupportedHttpAdapterRealtimeCapability('No-op HTTP adapter does not expose a server-backed realtime capability.');
39
+ },
40
+ async listen() {}
41
+ };
42
+ }
@@ -0,0 +1,11 @@
1
+ import { type Constructor } from '@fluojs/core';
2
+ import type { ArgumentResolverContext, Binder, Converter, ConverterLike, ConverterTarget } from '../types.js';
3
+ export declare class DefaultConverter implements Converter {
4
+ convert(value: unknown, _target: ConverterTarget): unknown;
5
+ }
6
+ export declare class DefaultBinder implements Binder {
7
+ private readonly converters;
8
+ constructor(converters?: readonly ConverterLike[]);
9
+ bind(dto: Constructor, context: ArgumentResolverContext): Promise<unknown>;
10
+ }
11
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAA6D,MAAM,cAAc,CAAC;AAK3H,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAC;AA4FhI,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;CAG3D;AAyDD,qBAAa,aAAc,YAAW,MAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,GAAE,SAAS,aAAa,EAAO;IAEhE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;CA0EjF"}
@@ -0,0 +1,185 @@
1
+ import { InvariantError } from '@fluojs/core';
2
+ import { getDtoBindingSchema } from '@fluojs/core/internal';
3
+ import { BadRequestException } from '../exceptions.js';
4
+ import { toInputErrorDetail } from '../input-error-detail.js';
5
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
6
+ function isPlainObject(value) {
7
+ if (typeof value !== 'object' || value === null) {
8
+ return false;
9
+ }
10
+ const prototype = Object.getPrototypeOf(value);
11
+ return prototype === Object.prototype || prototype === null;
12
+ }
13
+ function toFieldName(propertyKey) {
14
+ return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
15
+ }
16
+ function resolveSourceKey(propertyKey, key) {
17
+ return key ?? toFieldName(propertyKey);
18
+ }
19
+ function readHeader(request, key) {
20
+ return request.headers[key.toLowerCase()] ?? request.headers[key];
21
+ }
22
+ function readSourceValue(request, source, propertyKey, key) {
23
+ const resolvedKey = resolveSourceKey(propertyKey, key);
24
+ switch (source) {
25
+ case 'path':
26
+ return request.params[resolvedKey];
27
+ case 'query':
28
+ return request.query[resolvedKey];
29
+ case 'header':
30
+ return readHeader(request, resolvedKey);
31
+ case 'cookie':
32
+ return request.cookies[resolvedKey];
33
+ case 'body':
34
+ {
35
+ if (!isPlainObject(request.body)) {
36
+ if (request.body !== undefined && request.body !== null) {
37
+ throw new BadRequestException('Request body must be a plain object.', {
38
+ details: [toInputErrorDetail({
39
+ code: 'INVALID_BODY',
40
+ message: 'Request body must be a plain object.',
41
+ source: 'body'
42
+ })]
43
+ });
44
+ }
45
+ return undefined;
46
+ }
47
+ return request.body[resolvedKey];
48
+ }
49
+ }
50
+ }
51
+ function validateBodyKeys(request, bodyKeys) {
52
+ if (request.body === undefined || request.body === null) {
53
+ return;
54
+ }
55
+ if (!isPlainObject(request.body)) {
56
+ throw new BadRequestException('Request body must be a plain object.', {
57
+ details: [toInputErrorDetail({
58
+ code: 'INVALID_BODY',
59
+ message: 'Request body must be a plain object.',
60
+ source: 'body'
61
+ })]
62
+ });
63
+ }
64
+ const details = [];
65
+ for (const key of Object.keys(request.body)) {
66
+ if (DANGEROUS_KEYS.has(key)) {
67
+ details.push(toInputErrorDetail({
68
+ code: 'DANGEROUS_KEY',
69
+ field: key,
70
+ message: `Dangerous body key ${key} is not allowed.`,
71
+ source: 'body'
72
+ }));
73
+ continue;
74
+ }
75
+ if (!bodyKeys.has(key)) {
76
+ details.push(toInputErrorDetail({
77
+ code: 'UNKNOWN_FIELD',
78
+ field: key,
79
+ message: `Unknown body field ${key}.`,
80
+ source: 'body'
81
+ }));
82
+ }
83
+ }
84
+ if (details.length > 0) {
85
+ throw new BadRequestException('Request body contains unsupported fields.', {
86
+ details
87
+ });
88
+ }
89
+ }
90
+ export class DefaultConverter {
91
+ convert(value, _target) {
92
+ return value;
93
+ }
94
+ }
95
+ function isConverter(value) {
96
+ return typeof value === 'object' && value !== null && 'convert' in value && typeof value.convert === 'function';
97
+ }
98
+ function isConverterToken(value) {
99
+ return typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
100
+ }
101
+ async function resolveConverter(value, context, cache) {
102
+ if (!value) {
103
+ return undefined;
104
+ }
105
+ if (cache.has(value)) {
106
+ return cache.get(value);
107
+ }
108
+ if (isConverter(value)) {
109
+ cache.set(value, value);
110
+ return value;
111
+ }
112
+ if (!isConverterToken(value)) {
113
+ throw new InvariantError('Converter metadata must be a converter instance or DI token.');
114
+ }
115
+ try {
116
+ const resolved = await context.requestContext.container.resolve(value);
117
+ if (!isConverter(resolved)) {
118
+ throw new InvariantError('Resolved converter token does not implement convert().');
119
+ }
120
+ cache.set(value, resolved);
121
+ return resolved;
122
+ } catch (error) {
123
+ if (typeof value === 'function') {
124
+ const instantiated = new value();
125
+ if (!isConverter(instantiated)) {
126
+ throw new InvariantError('Converter class must implement convert(value, target).');
127
+ }
128
+ cache.set(value, instantiated);
129
+ return instantiated;
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+ export class DefaultBinder {
135
+ constructor(converters = []) {
136
+ this.converters = converters;
137
+ }
138
+ async bind(dto, context) {
139
+ const schema = getDtoBindingSchema(dto);
140
+ const value = new dto();
141
+ const converterCache = new Map();
142
+ const bodyKeys = new Set(schema.filter(entry => entry.metadata.source === 'body').map(entry => resolveSourceKey(entry.propertyKey, entry.metadata.key)));
143
+ const globalConverters = (await Promise.all(this.converters.map(converter => resolveConverter(converter, context, converterCache)))).filter(converter => Boolean(converter));
144
+ validateBodyKeys(context.requestContext.request, bodyKeys);
145
+ const details = [];
146
+ for (const entry of schema) {
147
+ const rawValue = readSourceValue(context.requestContext.request, entry.metadata.source, entry.propertyKey, entry.metadata.key);
148
+ if (rawValue === undefined) {
149
+ if (entry.metadata.optional) {
150
+ continue;
151
+ }
152
+ details.push(toInputErrorDetail({
153
+ code: 'MISSING_FIELD',
154
+ field: toFieldName(entry.propertyKey),
155
+ message: `Missing required ${entry.metadata.source} field ${resolveSourceKey(entry.propertyKey, entry.metadata.key)}.`,
156
+ source: entry.metadata.source
157
+ }));
158
+ continue;
159
+ }
160
+ const target = {
161
+ dto,
162
+ handler: context.handler,
163
+ key: resolveSourceKey(entry.propertyKey, entry.metadata.key),
164
+ propertyKey: entry.propertyKey,
165
+ requestContext: context.requestContext,
166
+ source: entry.metadata.source
167
+ };
168
+ let convertedValue = rawValue;
169
+ for (const converter of globalConverters) {
170
+ convertedValue = await converter.convert(convertedValue, target);
171
+ }
172
+ const fieldConverter = await resolveConverter(entry.metadata.converter, context, converterCache);
173
+ if (fieldConverter) {
174
+ convertedValue = await fieldConverter.convert(convertedValue, target);
175
+ }
176
+ value[entry.propertyKey] = convertedValue;
177
+ }
178
+ if (details.length > 0) {
179
+ throw new BadRequestException('Request binding failed.', {
180
+ details
181
+ });
182
+ }
183
+ return value;
184
+ }
185
+ }
@@ -0,0 +1,10 @@
1
+ import { type Constructor } from '@fluojs/core';
2
+ import type { Validator } from '../types.js';
3
+ export declare class HttpDtoValidationAdapter implements Validator {
4
+ private readonly validator;
5
+ private throwBadRequestForValidationError;
6
+ private filterUnboundRequestDtoFields;
7
+ validate(value: unknown, target: Constructor): Promise<void>;
8
+ materialize<T>(value: unknown, target: Constructor<T>): Promise<T>;
9
+ }
10
+ //# sourceMappingURL=dto-validation-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dto-validation-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-validation-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMhD,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,aAAa,CAAC;AAE9D,qBAAa,wBAAyB,YAAW,SAAS;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IAExD,OAAO,CAAC,iCAAiC;IAMzC,OAAO,CAAC,6BAA6B;IAiB/B,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAWzE"}
@@ -0,0 +1,46 @@
1
+ import { getDtoBindingSchema } from '@fluojs/core/internal';
2
+ import { DefaultValidator as BaseDefaultValidator, DtoValidationError } from '@fluojs/validation';
3
+ import { BadRequestException } from '../exceptions.js';
4
+ import { toInputErrorDetail } from '../input-error-detail.js';
5
+ export class HttpDtoValidationAdapter {
6
+ validator = new BaseDefaultValidator();
7
+ throwBadRequestForValidationError(error) {
8
+ throw new BadRequestException(error.message, {
9
+ details: error.issues.map(issue => toInputErrorDetail(issue))
10
+ });
11
+ }
12
+ filterUnboundRequestDtoFields(value, target) {
13
+ if (typeof value !== 'object' || value === null) {
14
+ return value;
15
+ }
16
+ const source = value;
17
+ const filtered = Object.create(Object.getPrototypeOf(value));
18
+ for (const binding of getDtoBindingSchema(target)) {
19
+ if (Object.hasOwn(source, binding.propertyKey)) {
20
+ filtered[binding.propertyKey] = source[binding.propertyKey];
21
+ }
22
+ }
23
+ return filtered;
24
+ }
25
+ async validate(value, target) {
26
+ try {
27
+ const filteredValue = this.filterUnboundRequestDtoFields(value, target);
28
+ await this.validator.validate(filteredValue, target);
29
+ } catch (error) {
30
+ if (error instanceof DtoValidationError) {
31
+ this.throwBadRequestForValidationError(error);
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ async materialize(value, target) {
37
+ try {
38
+ return await this.validator.materialize(value, target);
39
+ } catch (error) {
40
+ if (error instanceof DtoValidationError) {
41
+ this.throwBadRequestForValidationError(error);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ }