spiceflow 1.9.0 → 1.10.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -2
  3. package/dist/_node-server-unsupported.d.ts +5 -0
  4. package/dist/_node-server-unsupported.d.ts.map +1 -0
  5. package/dist/_node-server-unsupported.js +6 -0
  6. package/dist/_node-server.d.ts +7 -0
  7. package/dist/_node-server.d.ts.map +1 -0
  8. package/dist/_node-server.js +77 -0
  9. package/dist/client/index.d.ts +2 -2
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +2 -2
  12. package/dist/client/types.d.ts +2 -2
  13. package/dist/client/types.d.ts.map +1 -1
  14. package/dist/client.test.js +2 -2
  15. package/dist/context.d.ts +3 -3
  16. package/dist/cors.d.ts +2 -2
  17. package/dist/cors.d.ts.map +1 -1
  18. package/dist/cors.js +5 -2
  19. package/dist/cors.test.js +2 -2
  20. package/dist/error.d.ts +0 -1
  21. package/dist/error.d.ts.map +1 -1
  22. package/dist/error.js +0 -10
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +2 -2
  25. package/dist/mcp-transport.d.ts +2 -2
  26. package/dist/mcp-transport.d.ts.map +1 -1
  27. package/dist/mcp-transport.js +1 -6
  28. package/dist/mcp.d.ts +3 -3
  29. package/dist/mcp.d.ts.map +1 -1
  30. package/dist/mcp.js +3 -3
  31. package/dist/middleware.test.js +8 -3
  32. package/dist/openapi.d.ts +3 -3
  33. package/dist/openapi.d.ts.map +1 -1
  34. package/dist/openapi.js +15 -2
  35. package/dist/openapi.test.js +8 -8
  36. package/dist/serialize.d.ts +2 -0
  37. package/dist/serialize.d.ts.map +1 -0
  38. package/dist/serialize.js +9 -0
  39. package/dist/simple.benchmark.js +1 -1
  40. package/dist/spiceflow.d.ts +15 -8
  41. package/dist/spiceflow.d.ts.map +1 -1
  42. package/dist/spiceflow.js +30 -86
  43. package/dist/spiceflow.test.js +6 -4
  44. package/dist/static-node.d.ts +2 -2
  45. package/dist/static-node.d.ts.map +1 -1
  46. package/dist/static-node.js +1 -1
  47. package/dist/static.benchmark.js +2 -2
  48. package/dist/static.d.ts +1 -1
  49. package/dist/static.d.ts.map +1 -1
  50. package/dist/static.js +1 -1
  51. package/dist/stream.test.js +2 -2
  52. package/dist/types.d.ts +6 -6
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +1 -1
  55. package/dist/types.test.js +2 -2
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/zod.test.js +2 -2
  58. package/package.json +13 -12
  59. package/src/_node-server-unsupported.ts +20 -0
  60. package/src/_node-server.ts +115 -0
  61. package/src/client/index.ts +4 -4
  62. package/src/client/types.ts +47 -49
  63. package/src/client.test.ts +2 -3
  64. package/src/context.ts +3 -3
  65. package/src/cors.test.ts +11 -9
  66. package/src/cors.ts +7 -5
  67. package/src/error.ts +0 -12
  68. package/src/index.ts +3 -3
  69. package/src/mcp-transport.ts +2 -11
  70. package/src/mcp.ts +3 -4
  71. package/src/middleware.test.ts +19 -12
  72. package/src/openapi.test.ts +8 -8
  73. package/src/openapi.ts +20 -5
  74. package/src/serialize.ts +10 -0
  75. package/src/simple.benchmark.ts +1 -1
  76. package/src/spiceflow.test.ts +12 -10
  77. package/src/spiceflow.ts +70 -137
  78. package/src/static-node.ts +2 -2
  79. package/src/static.benchmark.ts +2 -2
  80. package/src/static.ts +2 -2
  81. package/src/stream.test.ts +2 -3
  82. package/src/types.test.ts +3 -3
  83. package/src/types.ts +18 -18
  84. package/src/zod.test.ts +2 -2
  85. package/dist/_node_utils.d.ts +0 -3
  86. package/dist/_node_utils.d.ts.map +0 -1
  87. package/dist/_node_utils.js +0 -2
  88. package/dist/_node_utils_browser.d.ts +0 -2
  89. package/dist/_node_utils_browser.d.ts.map +0 -1
  90. package/dist/_node_utils_browser.js +0 -3
  91. package/dist/mcp.test.d.ts +0 -2
  92. package/dist/mcp.test.d.ts.map +0 -1
  93. package/dist/mcp.test.js +0 -217
  94. package/src/_node_utils.ts +0 -2
  95. package/src/_node_utils_browser.ts +0 -3
  96. package/src/mcp.test.ts +0 -267
@@ -0,0 +1,115 @@
1
+ import {
2
+ type Server,
3
+ type IncomingMessage,
4
+ type ServerResponse,
5
+ createServer,
6
+ } from 'node:http'
7
+ import { AddressInfo } from 'node:net'
8
+ import { type Spiceflow, SpiceflowRequest } from './spiceflow.ts'
9
+ import { superjsonSerialize } from './serialize.ts'
10
+
11
+ export async function listenForNode(
12
+ app: Spiceflow<any, any, any, any, any, any>,
13
+ port: number,
14
+ hostname: string = '0.0.0.0',
15
+ ): Promise<Server<typeof IncomingMessage, typeof ServerResponse>> {
16
+ const server = createServer((req, res) => {
17
+ return app.handleForNode(req, res)
18
+ })
19
+
20
+ await new Promise((resolve) => {
21
+ server.listen(port, hostname, () => {
22
+ // We could print from what we take as arguments of `serve`, but by reading
23
+ // the `server` object, we can ensure that they are properly set.
24
+ const addressInfo = server.address() as AddressInfo
25
+ const displayedHost =
26
+ addressInfo.address === '0.0.0.0' ? 'localhost' : addressInfo.address
27
+ console.log(`Listening on http://${displayedHost}:${addressInfo.port}`)
28
+ resolve(null)
29
+ })
30
+ })
31
+
32
+ return server
33
+ }
34
+
35
+ export async function handleForNode(
36
+ app: Spiceflow<any, any, any, any, any, any>,
37
+ req: IncomingMessage,
38
+ res: ServerResponse,
39
+ context: { state?: {} | undefined } = {},
40
+ ): Promise<void> {
41
+ if (req?.['body']) {
42
+ throw new Error(
43
+ 'req.body is defined, you should disable your framework body parser to be able to use the request in Spiceflow',
44
+ )
45
+ }
46
+
47
+ const abortController = new AbortController()
48
+ const { signal } = abortController
49
+
50
+ req.on('error', (err) => {
51
+ abortController.abort()
52
+ })
53
+ req.on('aborted', (err) => {
54
+ abortController.abort()
55
+ })
56
+ res.on('close', function () {
57
+ let aborted = !res.writableFinished
58
+ if (aborted) {
59
+ abortController.abort()
60
+ }
61
+ })
62
+
63
+ const url = new URL(
64
+ req.url || '',
65
+ `http://${req.headers.host || 'localhost'}`,
66
+ )
67
+ const typedRequest = new SpiceflowRequest(url.toString(), {
68
+ method: req.method,
69
+ headers: req.headers as HeadersInit,
70
+ body:
71
+ req.method !== 'GET' && req.method !== 'HEAD'
72
+ ? new ReadableStream({
73
+ start(controller) {
74
+ req.on('data', (chunk) => {
75
+ controller.enqueue(
76
+ new Uint8Array(
77
+ chunk.buffer,
78
+ chunk.byteOffset,
79
+ chunk.byteLength,
80
+ ),
81
+ )
82
+ })
83
+ req.on('end', () => {
84
+ controller.close()
85
+ })
86
+ },
87
+ })
88
+ : null,
89
+ signal,
90
+ // @ts-ignore
91
+ duplex: 'half',
92
+ })
93
+
94
+ try {
95
+ const response = await app.handle(typedRequest, context)
96
+ res.writeHead(
97
+ response.status,
98
+ Object.fromEntries(response.headers.entries()),
99
+ )
100
+
101
+ if (response.body) {
102
+ const reader = response.body.getReader()
103
+ while (true) {
104
+ const { done, value } = await reader.read()
105
+ if (done) break
106
+ res.write(value)
107
+ }
108
+ }
109
+ res.end()
110
+ } catch (error) {
111
+ console.error('Error handling request:', error)
112
+ res.statusCode = 500
113
+ res.end(superjsonSerialize({ message: 'Internal Server Error' }))
114
+ }
115
+ }
@@ -1,17 +1,17 @@
1
1
  /* eslint-disable no-extra-semi */
2
2
  /* eslint-disable no-case-declarations */
3
3
  /* eslint-disable prefer-const */
4
- import type { Spiceflow } from '../spiceflow.js'
4
+ import type { Spiceflow } from '../spiceflow.ts'
5
5
  import superjson from 'superjson'
6
6
  import { EventSourceParserStream } from 'eventsource-parser/stream'
7
7
 
8
- import type { SpiceflowClient } from './types.js'
8
+ import type { SpiceflowClient } from './types.ts'
9
9
 
10
10
  export { SpiceflowClient }
11
11
 
12
- import { SpiceflowFetchError } from './errors.js'
12
+ import { SpiceflowFetchError } from './errors.ts'
13
13
 
14
- import { parseStringifiedValue } from './utils.js'
14
+ import { parseStringifiedValue } from './utils.ts'
15
15
 
16
16
  const method = [
17
17
  'get',
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom" />
2
- import type { Spiceflow } from '../spiceflow.js'
2
+ import type { Spiceflow } from '../spiceflow.ts'
3
3
 
4
- import { SpiceflowFetchError } from './errors.js'
4
+ import { SpiceflowFetchError } from './errors.ts'
5
5
 
6
6
  export type Prettify<T> = {
7
7
  [K in keyof T]: T[K]
@@ -15,11 +15,11 @@ type ReplaceBlobWithFiles<in out RecordType extends Record<string, unknown>> = {
15
15
  [K in keyof RecordType]: RecordType[K] extends any
16
16
  ? RecordType[K]
17
17
  : RecordType[K] extends
18
- | Blob
19
- | Blob[]
20
- | { arrayBuffer: () => Promise<ArrayBuffer> }
21
- ? Files
22
- : RecordType[K]
18
+ | Blob
19
+ | Blob[]
20
+ | { arrayBuffer: () => Promise<ArrayBuffer> }
21
+ ? Files
22
+ : RecordType[K]
23
23
  } & {}
24
24
 
25
25
  type And<A extends boolean, B extends boolean> = A extends true
@@ -34,18 +34,18 @@ type ReplaceGeneratorWithAsyncGenerator<
34
34
  [K in keyof RecordType]: RecordType[K] extends any
35
35
  ? RecordType[K]
36
36
  : RecordType[K] extends Generator<infer A, infer B, infer C>
37
- ? And<Not<IsNever<A>>, void extends B ? true : false> extends true
38
- ? AsyncGenerator<A, B, C>
39
- : And<IsNever<A>, void extends B ? false : true> extends true
40
- ? B
41
- : AsyncGenerator<A, B, C> | B
42
- : RecordType[K] extends AsyncGenerator<infer A, infer B, infer C>
43
- ? And<Not<IsNever<A>>, void extends B ? true : false> extends true
44
- ? AsyncGenerator<A, B, C>
45
- : And<IsNever<A>, void extends B ? false : true> extends true
46
- ? B
47
- : AsyncGenerator<A, B, C> | B
48
- : RecordType[K]
37
+ ? And<Not<IsNever<A>>, void extends B ? true : false> extends true
38
+ ? AsyncGenerator<A, B, C>
39
+ : And<IsNever<A>, void extends B ? false : true> extends true
40
+ ? B
41
+ : AsyncGenerator<A, B, C> | B
42
+ : RecordType[K] extends AsyncGenerator<infer A, infer B, infer C>
43
+ ? And<Not<IsNever<A>>, void extends B ? true : false> extends true
44
+ ? AsyncGenerator<A, B, C>
45
+ : And<IsNever<A>, void extends B ? false : true> extends true
46
+ ? B
47
+ : AsyncGenerator<A, B, C> | B
48
+ : RecordType[K]
49
49
  } & {}
50
50
 
51
51
  type MaybeArray<T> = T | T[]
@@ -97,40 +97,38 @@ export namespace SpiceflowClient {
97
97
  ClientResponse<ReplaceGeneratorWithAsyncGenerator<Response>>
98
98
  >
99
99
  : K extends 'get' | 'head'
100
- ? (
101
- options: Prettify<Param & ClientParam>,
102
- ) => Promise<
103
- ClientResponse<ReplaceGeneratorWithAsyncGenerator<Response>>
104
- >
105
- : (
106
- body: Body extends Record<string, unknown>
107
- ? ReplaceBlobWithFiles<Body>
108
- : Body,
109
- options: Prettify<Param & ClientParam>,
110
- ) => Promise<
111
- ClientResponse<ReplaceGeneratorWithAsyncGenerator<Response>>
112
- >
100
+ ? (
101
+ options: Prettify<Param & ClientParam>,
102
+ ) => Promise<
103
+ ClientResponse<ReplaceGeneratorWithAsyncGenerator<Response>>
104
+ >
105
+ : (
106
+ body: Body extends Record<string, unknown>
107
+ ? ReplaceBlobWithFiles<Body>
108
+ : Body,
109
+ options: Prettify<Param & ClientParam>,
110
+ ) => Promise<
111
+ ClientResponse<ReplaceGeneratorWithAsyncGenerator<Response>>
112
+ >
113
113
  : never
114
114
  : CreateParams<Route[K]>
115
115
  }
116
116
 
117
- type CreateParams<Route extends Record<string, any>> = Extract<
118
- keyof Route,
119
- `:${string}`
120
- > extends infer Path extends string
121
- ? IsNever<Path> extends true
122
- ? Prettify<Sign<Route>>
123
- : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED
124
- (((params: {
125
- [param in Path extends `:${infer Param}`
126
- ? Param extends `${infer Param}?`
127
- ? Param
128
- : Param
129
- : never]: string | number
130
- }) => Prettify<Sign<Route[Path]>> & CreateParams<Route[Path]>) &
131
- Prettify<Sign<Route>>) &
132
- (Path extends `:${string}?` ? CreateParams<Route[Path]> : {})
133
- : never
117
+ type CreateParams<Route extends Record<string, any>> =
118
+ Extract<keyof Route, `:${string}`> extends infer Path extends string
119
+ ? IsNever<Path> extends true
120
+ ? Prettify<Sign<Route>>
121
+ : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED
122
+ (((params: {
123
+ [param in Path extends `:${infer Param}`
124
+ ? Param extends `${infer Param}?`
125
+ ? Param
126
+ : Param
127
+ : never]: string | number
128
+ }) => Prettify<Sign<Route[Path]>> & CreateParams<Route[Path]>) &
129
+ Prettify<Sign<Route>>) &
130
+ (Path extends `:${string}?` ? CreateParams<Route[Path]> : {})
131
+ : never
134
132
 
135
133
  export interface Config {
136
134
  // fetch?: Omit<RequestInit, 'headers' | 'method'>
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { createSpiceflowClient } from './client/index.js'
3
- import { Spiceflow } from './spiceflow.js'
4
-
2
+ import { createSpiceflowClient } from './client/index.ts'
3
+ import { Spiceflow } from './spiceflow.ts'
5
4
 
6
5
  import { describe, expect, it } from 'vitest'
7
6
  const app = new Spiceflow()
package/src/context.ts CHANGED
@@ -2,7 +2,7 @@ import type {
2
2
  StatusMap,
3
3
  InvertedStatusMap,
4
4
  redirect as Redirect,
5
- } from './utils.js'
5
+ } from './utils.ts'
6
6
 
7
7
  import type {
8
8
  RouteSchema,
@@ -10,9 +10,9 @@ import type {
10
10
  ResolvePath,
11
11
  SingletonBase,
12
12
  HTTPHeaders,
13
- } from './types.js'
13
+ } from './types.ts'
14
14
 
15
- import { SpiceflowRequest } from './spiceflow.js'
15
+ import { SpiceflowRequest } from './spiceflow.ts'
16
16
 
17
17
  export type ErrorContext<
18
18
  in out Route extends RouteSchema = {},
package/src/cors.test.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from 'vitest'
2
2
  import { z } from 'zod'
3
- import { cors } from './cors.js'
4
- import { Spiceflow } from './spiceflow.js'
3
+ import { cors } from './cors.ts'
4
+ import { Spiceflow } from './spiceflow.ts'
5
5
 
6
6
  function request(path, method = 'GET') {
7
7
  return new Request(`http://localhost/${path}`, {
@@ -50,9 +50,9 @@ describe('cors middleware', () => {
50
50
  })
51
51
 
52
52
  test('CORS headers are set when an error is thrown', async () => {
53
- let errorRouteCallCount = 0;
53
+ let errorRouteCallCount = 0
54
54
  const errorApp = new Spiceflow().use(cors()).get('/error', () => {
55
- errorRouteCallCount++;
55
+ errorRouteCallCount++
56
56
  throw new Error('Test error')
57
57
  })
58
58
 
@@ -64,29 +64,31 @@ test('CORS headers are set when an error is thrown', async () => {
64
64
  })
65
65
 
66
66
  test('CORS headers are set for OPTIONS request when an error is thrown', async () => {
67
- let errorRouteCallCount = 0;
67
+ let errorRouteCallCount = 0
68
68
  const errorApp = new Spiceflow().use(cors()).options('/error', () => {
69
- errorRouteCallCount++;
69
+ errorRouteCallCount++
70
70
  throw new Error('Test error')
71
71
  })
72
72
 
73
73
  const res = await errorApp.handle(request('error', 'OPTIONS'))
74
74
  expect(res.status).toBe(204)
75
75
  expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*')
76
- expect(res.headers.get('Access-Control-Allow-Methods')).toBe('GET,HEAD,PUT,POST,DELETE,PATCH')
76
+ expect(res.headers.get('Access-Control-Allow-Methods')).toBe(
77
+ 'GET,HEAD,PUT,POST,DELETE,PATCH',
78
+ )
77
79
  expect(errorRouteCallCount).toBe(1)
78
80
  })
79
81
 
80
82
  // TODO should middleware errors be handled? errors can be a way to short circuit other middlewares
81
83
  test('CORS headers are set when an error is thrown in middleware', async () => {
82
- let errorRouteCallCount = 0;
84
+ let errorRouteCallCount = 0
83
85
  const errorApp = new Spiceflow()
84
86
  .use((c) => {
85
87
  throw new Error('middleware error')
86
88
  })
87
89
  .use(cors())
88
90
  .get('/error', () => {
89
- errorRouteCallCount++;
91
+ errorRouteCallCount++
90
92
  throw new Error('Test error')
91
93
  })
92
94
 
package/src/cors.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MiddlewareHandler } from './types.js'
1
+ import { MiddlewareHandler } from './types.ts'
2
2
  /**
3
3
  * Options for configuring CORS (Cross-Origin Resource Sharing) middleware.
4
4
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS MDN CORS documentation}
@@ -15,9 +15,10 @@ type CORSOptions = {
15
15
  /** Configures the Access-Control-Allow-Credentials CORS header */
16
16
  credentials?: boolean
17
17
  /** Configures the Access-Control-Expose-Headers CORS header */
18
- exposeHeaders?: string[]
18
+ exposeHeaders?: string[] | boolean
19
19
  /** Configures browser and CDN caching duration for CORS preflight requests in seconds. Set to 0 to disable. */
20
20
  cacheAge?: number
21
+
21
22
  }
22
23
 
23
24
  export const cors = (options?: CORSOptions): MiddlewareHandler => {
@@ -25,8 +26,8 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => {
25
26
  origin: '*',
26
27
  allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'],
27
28
  allowHeaders: [],
28
- exposeHeaders: [],
29
29
  credentials: true,
30
+ exposeHeaders: true,
30
31
  cacheAge: 21600, // 6 hours default
31
32
  }
32
33
  const opts = {
@@ -81,8 +82,9 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => {
81
82
  if (opts.credentials) {
82
83
  set('Access-Control-Allow-Credentials', 'true')
83
84
  }
84
-
85
- if (opts.exposeHeaders?.length) {
85
+ if (opts.exposeHeaders === true) {
86
+ set('Access-Control-Expose-Headers', '*')
87
+ } else if (opts.exposeHeaders && opts.exposeHeaders?.length) {
86
88
  set('Access-Control-Expose-Headers', opts.exposeHeaders.join(','))
87
89
  }
88
90
 
package/src/error.ts CHANGED
@@ -1,21 +1,9 @@
1
- // ? Cloudflare worker support
2
- const env =
3
- // @ts-ignore
4
- typeof Bun !== 'undefined'
5
- ? // @ts-ignore
6
- Bun.env
7
- : typeof process !== 'undefined'
8
- ? process?.env
9
- : undefined
10
-
11
1
  export const ERROR_CODE = Symbol('SpiceflowErrorCode')
12
2
  export type ERROR_CODE = typeof ERROR_CODE
13
3
 
14
4
  export const SPICEFLOW_RESPONSE = Symbol('SpiceflowResponse')
15
5
  export type SPICEFLOW_RESPONSE = typeof SPICEFLOW_RESPONSE
16
6
 
17
- export const isProduction = (env?.NODE_ENV ?? env?.ENV) === 'production'
18
-
19
7
  export class ValidationError extends Error {
20
8
  code = 'VALIDATION'
21
9
  status = 422
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { Spiceflow } from './spiceflow.js'
2
- export type { AnySpiceflow } from './spiceflow.js'
3
- export { InternalServerError, ParseError, ValidationError } from './error.js'
1
+ export { Spiceflow } from './spiceflow.ts'
2
+ export type { AnySpiceflow } from './spiceflow.ts'
3
+ export { InternalServerError, ParseError, ValidationError } from './error.ts'
@@ -1,5 +1,5 @@
1
1
  // https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/server/sse.ts
2
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
2
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.ts'
3
3
  import {
4
4
  JSONRPCMessage,
5
5
  JSONRPCMessageSchema,
@@ -65,12 +65,6 @@ export class SSEServerTransportSpiceflow implements Transport {
65
65
  }\n\n`,
66
66
  ),
67
67
  )
68
-
69
- // readable.getReader().closed.then(() => {
70
- // this.response = undefined
71
- // this._writableStream = undefined
72
- // this.onclose?.()
73
- // })
74
68
  }
75
69
 
76
70
  /**
@@ -78,10 +72,7 @@ export class SSEServerTransportSpiceflow implements Transport {
78
72
  *
79
73
  * This should be called when a POST request is made to send a message to the server.
80
74
  */
81
- async handlePostMessage(
82
- req: Request,
83
- parsedBody?: unknown,
84
- ): Promise<Response> {
75
+ async handlePostMessage(req: Request): Promise<Response> {
85
76
  if (!this.response) {
86
77
  const message = 'SSE connection not established'
87
78
  throw new Error(message)
package/src/mcp.ts CHANGED
@@ -7,10 +7,9 @@ import {
7
7
  ReadResourceRequestSchema,
8
8
  } from '@modelcontextprotocol/sdk/types.js'
9
9
  import { OpenAPIV3 } from 'openapi-types'
10
- import { SSEServerTransportSpiceflow } from './mcp-transport.js'
11
- import { openapi } from './openapi.js'
12
- import { Spiceflow } from './spiceflow.js'
13
-
10
+ import { SSEServerTransportSpiceflow } from './mcp-transport.ts'
11
+ import { openapi } from './openapi.ts'
12
+ import { Spiceflow } from './spiceflow.ts'
14
13
 
15
14
  const transports = new Map<string, SSEServerTransportSpiceflow>()
16
15
  function getOperationRequestBody(
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from 'vitest'
2
2
  import { z } from 'zod'
3
- import { Spiceflow } from './spiceflow.js'
3
+ import { Spiceflow } from './spiceflow.ts'
4
4
 
5
5
  test('middleware with next changes the response', async () => {
6
6
  const res = await new Spiceflow()
@@ -36,7 +36,6 @@ test('middleware with no handlers works', async () => {
36
36
  expect(await res.text()).toEqual('ok')
37
37
  })
38
38
 
39
-
40
39
  test('middleware calling next() without returning it works', async () => {
41
40
  const res = await new Spiceflow()
42
41
  .use(async ({ request }, next) => {
@@ -331,7 +330,6 @@ test('middleware returning response and middleware adding header with mounted Sp
331
330
  expect(res.headers.get('X-Added-Header')).toBe('HeaderValue')
332
331
  })
333
332
 
334
-
335
333
  test('each middleware and route is called exactly once if an error is thrown', async () => {
336
334
  const callOrder: string[] = []
337
335
 
@@ -357,20 +355,29 @@ test('each middleware and route is called exactly once if an error is thrown', a
357
355
  const res = await app.handle(new Request('http://localhost/test'))
358
356
 
359
357
  expect(res.status).toBe(500)
360
- expect(await res.text()).toMatchInlineSnapshot(`"{"message":"Route response"}"`)
361
- expect(callOrder).toEqual(['middleware1', 'middleware2', 'middleware3', 'route'])
362
-
358
+ expect(await res.text()).toMatchInlineSnapshot(
359
+ `"{"message":"Route response"}"`,
360
+ )
361
+ expect(callOrder).toEqual([
362
+ 'middleware1',
363
+ 'middleware2',
364
+ 'middleware3',
365
+ 'route',
366
+ ])
367
+
363
368
  // Check that each middleware and route is called exactly once
364
- const counts = callOrder.reduce((acc, item) => {
365
- acc[item] = (acc[item] || 0) + 1
366
- return acc
367
- }, {} as Record<string, number>)
369
+ const counts = callOrder.reduce(
370
+ (acc, item) => {
371
+ acc[item] = (acc[item] || 0) + 1
372
+ return acc
373
+ },
374
+ {} as Record<string, number>,
375
+ )
368
376
 
369
377
  expect(counts).toEqual({
370
378
  middleware1: 1,
371
379
  middleware2: 1,
372
380
  middleware3: 1,
373
- route: 1
381
+ route: 1,
374
382
  })
375
383
  })
376
-
@@ -1,10 +1,11 @@
1
1
  import { expect, test } from 'vitest'
2
- import { Spiceflow } from './spiceflow.js'
3
- import { openapi } from './openapi.js'
4
2
  import { z } from 'zod'
3
+ import { z as z4 } from 'zod/v4'
4
+ import { openapi } from './openapi.ts'
5
+ import { Spiceflow } from './spiceflow.ts'
5
6
 
6
7
  test('openapi response', async () => {
7
- const app = await new Spiceflow()
8
+ const app = new Spiceflow()
8
9
  .use(
9
10
  openapi({
10
11
  info: {
@@ -43,8 +44,8 @@ test('openapi response', async () => {
43
44
  return body
44
45
  },
45
46
  {
46
- body: z.object({
47
- name: z.string(),
47
+ body: z4.object({
48
+ name: z4.string(),
48
49
  }),
49
50
  response: z.object({
50
51
  name: z.string().optional(),
@@ -148,8 +149,8 @@ test('openapi response', async () => {
148
149
  '/ids/:id',
149
150
  ({ params }) => params.id,
150
151
  {
151
- params: z.object({
152
- id: z.string(),
152
+ params: z4.object({
153
+ id: z4.string(),
153
154
  }),
154
155
  },
155
156
  ),
@@ -243,7 +244,6 @@ test('openapi response', async () => {
243
244
  "content": {
244
245
  "application/json": {
245
246
  "schema": {
246
- "additionalProperties": true,
247
247
  "properties": {
248
248
  "name": {
249
249
  "type": "string",
package/src/openapi.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { JSONSchemaType } from 'ajv'
2
- import { InternalRoute, isZodSchema, Spiceflow } from './spiceflow.js'
1
+ import { InternalRoute, isZod4, isZodSchema, Spiceflow } from './spiceflow.ts'
3
2
 
4
3
  import type { OpenAPIV3 } from 'openapi-types'
5
4
 
6
5
  let excludeMethods = ['OPTIONS']
7
6
 
8
- import type { TypeSchema } from './types.js'
7
+ import type { TypeSchema } from './types.ts'
9
8
 
10
- import { z } from 'zod'
11
9
  import { zodToJsonSchema } from 'zod-to-json-schema'
10
+ import { z } from 'zod/v4'
12
11
 
13
12
  const extractParamNames = (path: string): string[] => {
14
13
  return path.split('/').reduce((params: string[], segment) => {
@@ -455,8 +454,24 @@ export const openapi = <Path extends string = '/openapi'>({
455
454
  return app
456
455
  }
457
456
 
458
- function getJsonSchema(schema: TypeSchema): JSONSchemaType<any> {
457
+ function getJsonSchema(schema: TypeSchema) {
459
458
  if (!schema) return undefined as any
459
+
460
+ if (isZod4(schema)) {
461
+ let jsonSchema = z.toJSONSchema(schema, {
462
+ override(ctx) {
463
+ const schema = ctx.zodSchema
464
+ if (
465
+ schema instanceof z.core.$ZodObject &&
466
+ schema._zod.def.catchall === undefined
467
+ ) {
468
+ delete ctx.jsonSchema.additionalProperties
469
+ }
470
+ },
471
+ })
472
+ const { $schema, ...rest } = jsonSchema
473
+ return rest as any
474
+ }
460
475
  if (isZodSchema(schema)) {
461
476
  let jsonSchema = zodToJsonSchema(schema, {
462
477
  removeAdditionalStrategy: 'strict',
@@ -0,0 +1,10 @@
1
+ import superjson from 'superjson'
2
+
3
+ export function superjsonSerialize(value: any, indent = false) {
4
+ // return JSON.stringify(value)
5
+ const { json, meta } = superjson.serialize(value)
6
+ if (json && meta) {
7
+ json['__superjsonMeta'] = meta
8
+ }
9
+ return JSON.stringify(json ?? null, null, indent ? 2 : undefined)
10
+ }
@@ -1,6 +1,6 @@
1
1
  import { bench } from 'vitest'
2
2
 
3
- import { Spiceflow } from './spiceflow.js'
3
+ import { Spiceflow } from './spiceflow.ts'
4
4
 
5
5
  bench('Spiceflow basic routing', async () => {
6
6
  const app = new Spiceflow()