@fluojs/platform-fastify 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -43,7 +43,7 @@ await app.listen();
43
43
  ## 주요 패턴
44
44
 
45
45
  ### 멀티파트 및 Raw Body
46
- Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
46
+ Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
47
47
 
48
48
  ```typescript
49
49
  const adapter = createFastifyAdapter(
@@ -65,6 +65,9 @@ Fastify는 `@fluojs/websockets`가 기본 Node.js HTTP 서버에 직접 연결
65
65
  export class MyGateway {}
66
66
  ```
67
67
 
68
+ ### 스트리밍 응답
69
+ Fastify 기반 응답 스트림은 SSE 및 기타 스트리밍 writer가 사용하는 공통 fluo 스트림 계약을 지원합니다. `response.stream.waitForDrain()`은 기본 응답이 `drain`, `close`, 또는 `error`를 내보낼 때 settle되므로, 클라이언트가 연결을 끊거나 backpressure가 해소되기 전에 스트림이 닫혀도 writer가 멈춰 있지 않습니다.
70
+
68
71
  ### CORS 설정
69
72
  CORS는 부트스트랩 옵션을 통해 처리됩니다. fluo는 별도의 Fastify 플러그인에 의존하지 않고 내부 CORS 로직을 관리합니다.
70
73
 
@@ -122,6 +125,13 @@ await bootstrapFastifyApplication(AppModule, {
122
125
  });
123
126
  ```
124
127
 
128
+ ### 네이티브 라우트 등록과 안전한 폴백
129
+ fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 Fastify 네이티브 per-route 핸들러를 등록합니다. 이 네이티브 핸들러도 최종적으로는 공통 fluo dispatcher로 위임하므로 params, versioning, middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
130
+
131
+ 여러 라우트가 같은 method와 정규화된 param shape를 공유하는 경우(예: `/:id` 와 `/:slug`), 어댑터는 해당 shape를 의도적으로 와일드카드 fallback 경로에 남겨 둡니다. 이렇게 해서 Fastify 등록 단계에서 부팅 실패가 나거나 fluo의 등록 순서 기반 매칭 의미론이 좁아지지 않도록 보장합니다.
132
+
133
+ 어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다.
134
+
125
135
  ## 성능
126
136
 
127
137
  fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어댑터보다 훨씬 뛰어난 성능을 발휘합니다.
package/README.md CHANGED
@@ -43,7 +43,7 @@ await app.listen();
43
43
  ## Common Patterns
44
44
 
45
45
  ### Multipart and Raw Body
46
- The Fastify adapter includes built-in support for multipart form-data and raw body parsing via internal Fastify plugins, exposed through the standard fluo request interface. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same multipart settings under `options.multipart`.
46
+ The Fastify adapter includes built-in support for multipart form-data and raw body parsing via internal Fastify plugins, exposed through the standard fluo request interface. When `rawBody: true` is enabled, `FrameworkRequest.rawBody` preserves the original request bytes for non-multipart requests so webhook signature verification and other byte-sensitive flows can replay the exact payload. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same multipart settings under `options.multipart`.
47
47
 
48
48
  ```typescript
49
49
  const adapter = createFastifyAdapter(
@@ -65,6 +65,9 @@ Fastify provides a `server-backed` capability that allows `@fluojs/websockets` t
65
65
  export class MyGateway {}
66
66
  ```
67
67
 
68
+ ### Streaming Responses
69
+ Fastify-backed response streams support the shared fluo stream contract used by SSE and other streaming writers. `response.stream.waitForDrain()` settles when the underlying response emits `drain`, `close`, or `error`, so writers do not hang when a client disconnects or the stream closes before backpressure clears.
70
+
68
71
  ### CORS Configuration
69
72
  CORS is handled via bootstrap options. fluo manages the underlying CORS logic rather than relying on a separate Fastify plugin.
70
73
 
@@ -122,6 +125,13 @@ await bootstrapFastifyApplication(AppModule, {
122
125
  });
123
126
  ```
124
127
 
128
+ ### Native Route Registration with Safe Fallback
129
+ When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers instead of sending every request through a single wildcard route. Those native handlers still hand off to the shared fluo dispatcher, so params, versioning, middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, and error handling keep the same framework-level semantics.
130
+
131
+ When multiple routes share the same method and normalized param shape (for example `/:id` and `/:slug`), the adapter intentionally leaves that shape on the wildcard fallback path so Fastify registration cannot boot-fail or narrow fluo's registration-order matching semantics.
132
+
133
+ The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, and enables Fastify trailing-slash / duplicate-slash normalization so native selection stays aligned with fluo's documented route path contract.
134
+
125
135
  ## Performance
126
136
 
127
137
  fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high-concurrency scenarios.
package/dist/adapter.d.ts CHANGED
@@ -83,7 +83,9 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
83
83
  getListenTarget(): FastifyListenTarget;
84
84
  listen(dispatcher: Dispatcher): Promise<void>;
85
85
  close(): Promise<void>;
86
- private registerPluginsAndRoute;
86
+ private registerPluginsAndRoutes;
87
+ private registerNativeRoutes;
88
+ private registerWildcardFallbackRoute;
87
89
  private listenWithRetry;
88
90
  private handleRequest;
89
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAOtE,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,UAAU,EAIf,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAczB,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,0EAA0E;AAC1E,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC5D,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,WAAW,CAAC;AAKhE;;;GAGG;AACH,MAAM,WAAW,kCAAmC,SAAQ,IAAI,CAAC,wBAAwB,EAAE,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC7H,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,KAAK,GAAG,sBAAsB,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,4BAA6B,SAAQ,kCAAkC;IACtF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,KAAK,GAAG,SAAS,wBAAwB,EAAE,CAAC;CAC/D;AAED,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAYD;;;;;GAKG;AACH,qBAAa,6BAA8B,YAAW,sBAAsB;IAYxE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAnBpC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAIrC;gBAGiB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,oBAAM,EAClB,UAAU,oBAAK,EACf,YAAY,EAAE,kBAAkB,GAAG,SAAS,EAC5C,gBAAgB,CAAC,EAAE,gBAAgB,YAAA,EACnC,WAAW,SAAwB,EACnC,eAAe,UAAQ,EACvB,iBAAiB,SAA8B;IAUlE,SAAS,IAAI,OAAO;IAIpB,qBAAqB;IAIrB,eAAe,IAAI,mBAAmB;IAIhC,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAyBd,uBAAuB;YAuBvB,eAAe;YAkBf,aAAa;CAS5B;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,qBAA0B,EACnC,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,sBAAsB,CAYxB;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,WAAW,CAAC,CAQtB;AAqOD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgBvE"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { Transform } from 'node:stream';
1
2
  import multipart from '@fastify/multipart';
2
3
  import fastify from 'fastify';
3
- import fastifyRawBody from 'fastify-raw-body';
4
4
  import { createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
5
5
  import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
6
6
  import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
@@ -16,6 +16,7 @@ import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/req
16
16
 
17
17
  const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
18
18
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
19
+ const FASTIFY_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
19
20
 
20
21
  /**
21
22
  * Bootstrap options for creating a Fastify-backed application without
@@ -62,7 +63,7 @@ export class FastifyHttpApplicationAdapter {
62
63
  }
63
64
  async listen(dispatcher) {
64
65
  this.dispatcher = dispatcher;
65
- await this.registerPluginsAndRoute();
66
+ await this.registerPluginsAndRoutes(dispatcher);
66
67
  await this.listenWithRetry();
67
68
  }
68
69
  async close() {
@@ -85,23 +86,33 @@ export class FastifyHttpApplicationAdapter {
85
86
  }
86
87
  await waitForCloseWithTimeout(closeInFlight, this.shutdownTimeoutMs);
87
88
  }
88
- async registerPluginsAndRoute() {
89
+ async registerPluginsAndRoutes(dispatcher) {
89
90
  if (this.pluginsReady) {
90
91
  return;
91
92
  }
92
93
  await this.app.register(multipart);
93
94
  if (this.preserveRawBody) {
94
- await this.app.register(fastifyRawBody, {
95
- encoding: 'utf8',
96
- field: 'rawBody',
97
- global: true,
98
- runFirst: true
95
+ this.app.addHook('preParsing', captureRawBodyPreParsingHook);
96
+ }
97
+ this.registerNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher));
98
+ this.registerWildcardFallbackRoute();
99
+ this.pluginsReady = true;
100
+ }
101
+ registerNativeRoutes(descriptors) {
102
+ for (const route of createFastifyNativeRoutes(descriptors)) {
103
+ this.app.route({
104
+ handler: async (request, reply) => {
105
+ await this.handleRequest(request, reply);
106
+ },
107
+ method: route.method,
108
+ url: route.path
99
109
  });
100
110
  }
111
+ }
112
+ registerWildcardFallbackRoute() {
101
113
  this.app.all('*', async (request, reply) => {
102
114
  await this.handleRequest(request, reply);
103
115
  });
104
- this.pluginsReady = true;
105
116
  }
106
117
  async listenWithRetry() {
107
118
  for (let attempt = 0;; attempt++) {
@@ -150,6 +161,51 @@ function createFastifyRequestResponseFactory(multipartOptions, maxBodySize = DEF
150
161
  }
151
162
  };
152
163
  }
164
+ function resolveDispatcherRouteDescriptors(dispatcher) {
165
+ return dispatcher.describeRoutes?.() ?? [];
166
+ }
167
+ function createFastifyNativeRoutes(descriptors) {
168
+ const candidates = new Map();
169
+ const shapePaths = new Map();
170
+ for (const descriptor of descriptors) {
171
+ const path = descriptor.route.path;
172
+ if (descriptor.route.method === 'ALL') {
173
+ for (const method of FASTIFY_NATIVE_ROUTE_METHODS) {
174
+ registerFastifyNativeRouteCandidate(candidates, shapePaths, method, path);
175
+ }
176
+ continue;
177
+ }
178
+ registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor.route.method, path);
179
+ }
180
+ return [...candidates.values()].filter(candidate => shapePaths.get(candidate.shapeKey)?.size === 1).map(({
181
+ method,
182
+ path
183
+ }) => ({
184
+ method,
185
+ path
186
+ }));
187
+ }
188
+ function registerFastifyNativeRouteCandidate(candidates, shapePaths, method, path) {
189
+ const routeKey = `${method}:${path}`;
190
+ const shapeKey = `${method}:${canonicalizeFastifyRouteShape(path)}`;
191
+ if (!candidates.has(routeKey)) {
192
+ candidates.set(routeKey, {
193
+ method,
194
+ path,
195
+ shapeKey
196
+ });
197
+ }
198
+ let paths = shapePaths.get(shapeKey);
199
+ if (!paths) {
200
+ paths = new Set();
201
+ shapePaths.set(shapeKey, paths);
202
+ }
203
+ paths.add(path);
204
+ }
205
+ function canonicalizeFastifyRouteShape(path) {
206
+ const segments = path.split('/').filter(Boolean).map(segment => segment.startsWith(':') ? ':' : segment);
207
+ return segments.length === 0 ? '/' : `/${segments.join('/')}`;
208
+ }
153
209
 
154
210
  /**
155
211
  * Create the recommended Fastify adapter for `FluoFactory.create(...)`.
@@ -278,11 +334,19 @@ function createFrameworkResponseStream(reply) {
278
334
  },
279
335
  waitForDrain() {
280
336
  ensureHijacked();
281
- if (reply.raw.writableEnded) {
337
+ if (reply.raw.writableEnded || reply.raw.destroyed) {
282
338
  return Promise.resolve();
283
339
  }
284
340
  return new Promise(resolve => {
285
- reply.raw.once('drain', () => resolve());
341
+ const settle = () => {
342
+ reply.raw.removeListener('drain', settle);
343
+ reply.raw.removeListener('close', settle);
344
+ reply.raw.removeListener('error', settle);
345
+ resolve();
346
+ };
347
+ reply.raw.once('drain', settle);
348
+ reply.raw.once('close', settle);
349
+ reply.raw.once('error', settle);
286
350
  });
287
351
  },
288
352
  write(chunk) {
@@ -325,7 +389,7 @@ async function createFrameworkRequest(request, signal, multipartOptions, maxBody
325
389
  if (preserveRawBody && !isMultipart) {
326
390
  const rawBodyValue = request.rawBody;
327
391
  if (rawBodyValue !== undefined) {
328
- frameworkRequest.rawBody = typeof rawBodyValue === 'string' ? Buffer.from(rawBodyValue, 'utf8') : rawBodyValue;
392
+ frameworkRequest.rawBody = rawBodyValue;
329
393
  }
330
394
  }
331
395
  return frameworkRequest;
@@ -490,15 +554,54 @@ function createFastifyApp(httpsOptions, maxBodySize) {
490
554
  if (httpsOptions) {
491
555
  return fastify({
492
556
  bodyLimit: maxBodySize,
557
+ exposeHeadRoutes: false,
493
558
  https: httpsOptions,
494
- logger: false
559
+ logger: false,
560
+ routerOptions: {
561
+ ignoreDuplicateSlashes: true,
562
+ ignoreTrailingSlash: true
563
+ }
495
564
  });
496
565
  }
497
566
  return fastify({
498
567
  bodyLimit: maxBodySize,
499
- logger: false
568
+ exposeHeadRoutes: false,
569
+ logger: false,
570
+ routerOptions: {
571
+ ignoreDuplicateSlashes: true,
572
+ ignoreTrailingSlash: true
573
+ }
500
574
  });
501
575
  }
576
+ function captureRawBodyPreParsingHook(request, _reply, payload, done) {
577
+ if (isMultipartRequestContentType(request.headers['content-type'])) {
578
+ done(null, payload);
579
+ return;
580
+ }
581
+ const chunks = [];
582
+ const capture = new Transform({
583
+ transform(chunk, _encoding, callback) {
584
+ const bufferChunk = Buffer.isBuffer(chunk) ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk) : Buffer.from(String(chunk), 'utf8');
585
+ chunks.push(bufferChunk);
586
+ callback(null, chunk);
587
+ },
588
+ flush(callback) {
589
+ if (chunks.length > 0) {
590
+ request.rawBody = Buffer.concat(chunks);
591
+ }
592
+ callback();
593
+ }
594
+ });
595
+ payload.on('error', error => {
596
+ capture.destroy(error);
597
+ });
598
+ payload.pipe(capture);
599
+ done(null, capture);
600
+ }
601
+ function isMultipartRequestContentType(contentType) {
602
+ const primaryValue = Array.isArray(contentType) ? contentType[0] : contentType;
603
+ return typeof primaryValue === 'string' && primaryValue.includes('multipart/form-data');
604
+ }
502
605
  function resolveListenTarget(address, port, host, useHttps) {
503
606
  const protocol = useHttps ? 'https' : 'http';
504
607
  const resolvedPort = typeof address === 'object' && address !== null ? address.port : port;
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "platform",
9
9
  "server"
10
10
  ],
11
- "version": "1.0.0-beta.2",
11
+ "version": "1.0.0-beta.4",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -38,8 +38,8 @@
38
38
  "@fastify/multipart": "^9.2.1",
39
39
  "fastify": "^5.8.5",
40
40
  "fastify-raw-body": "^5.0.0",
41
- "@fluojs/http": "^1.0.0-beta.1",
42
- "@fluojs/runtime": "^1.0.0-beta.1"
41
+ "@fluojs/http": "^1.0.0-beta.3",
42
+ "@fluojs/runtime": "^1.0.0-beta.4"
43
43
  },
44
44
  "devDependencies": {
45
45
  "vitest": "^3.2.4"