spiceflow 1.9.1 → 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.
- package/LICENSE +21 -0
- package/README.md +1 -2
- package/dist/_node-server-unsupported.d.ts +5 -0
- package/dist/_node-server-unsupported.d.ts.map +1 -0
- package/dist/_node-server-unsupported.js +6 -0
- package/dist/_node-server.d.ts +7 -0
- package/dist/_node-server.d.ts.map +1 -0
- package/dist/_node-server.js +77 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/types.d.ts +2 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client.test.js +2 -2
- package/dist/context.d.ts +3 -3
- package/dist/cors.d.ts +2 -2
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +5 -2
- package/dist/cors.test.js +2 -2
- package/dist/error.d.ts +0 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +0 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mcp-transport.d.ts +2 -2
- package/dist/mcp-transport.d.ts.map +1 -1
- package/dist/mcp-transport.js +1 -6
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/middleware.test.js +8 -3
- package/dist/openapi.d.ts +3 -3
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +15 -2
- package/dist/openapi.test.js +8 -8
- package/dist/serialize.d.ts +2 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +9 -0
- package/dist/simple.benchmark.js +1 -1
- package/dist/spiceflow.d.ts +15 -8
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +27 -86
- package/dist/spiceflow.test.js +6 -4
- package/dist/static-node.d.ts +2 -2
- package/dist/static-node.d.ts.map +1 -1
- package/dist/static-node.js +1 -1
- package/dist/static.benchmark.js +2 -2
- package/dist/static.d.ts +1 -1
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +1 -1
- package/dist/stream.test.js +2 -2
- package/dist/types.d.ts +6 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.test.js +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/zod.test.js +2 -2
- package/package.json +13 -12
- package/src/_node-server-unsupported.ts +20 -0
- package/src/_node-server.ts +115 -0
- package/src/client/index.ts +4 -4
- package/src/client/types.ts +47 -49
- package/src/client.test.ts +2 -3
- package/src/context.ts +3 -3
- package/src/cors.test.ts +11 -9
- package/src/cors.ts +7 -5
- package/src/error.ts +0 -12
- package/src/index.ts +3 -3
- package/src/mcp-transport.ts +2 -11
- package/src/mcp.ts +3 -4
- package/src/middleware.test.ts +19 -12
- package/src/openapi.test.ts +8 -8
- package/src/openapi.ts +20 -5
- package/src/serialize.ts +10 -0
- package/src/simple.benchmark.ts +1 -1
- package/src/spiceflow.test.ts +12 -10
- package/src/spiceflow.ts +67 -137
- package/src/static-node.ts +2 -2
- package/src/static.benchmark.ts +2 -2
- package/src/static.ts +2 -2
- package/src/stream.test.ts +2 -3
- package/src/types.test.ts +3 -3
- package/src/types.ts +18 -18
- package/src/zod.test.ts +2 -2
- package/dist/_node_utils.d.ts +0 -3
- package/dist/_node_utils.d.ts.map +0 -1
- package/dist/_node_utils.js +0 -2
- package/dist/_node_utils_browser.d.ts +0 -2
- package/dist/_node_utils_browser.d.ts.map +0 -1
- package/dist/_node_utils_browser.js +0 -3
- package/dist/mcp.test.d.ts +0 -2
- package/dist/mcp.test.d.ts.map +0 -1
- package/dist/mcp.test.js +0 -217
- package/src/_node_utils.ts +0 -2
- package/src/_node_utils_browser.ts +0 -3
- 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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
8
|
+
import type { SpiceflowClient } from './types.ts'
|
|
9
9
|
|
|
10
10
|
export { SpiceflowClient }
|
|
11
11
|
|
|
12
|
-
import { SpiceflowFetchError } from './errors.
|
|
12
|
+
import { SpiceflowFetchError } from './errors.ts'
|
|
13
13
|
|
|
14
|
-
import { parseStringifiedValue } from './utils.
|
|
14
|
+
import { parseStringifiedValue } from './utils.ts'
|
|
15
15
|
|
|
16
16
|
const method = [
|
|
17
17
|
'get',
|
package/src/client/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import type { Spiceflow } from '../spiceflow.
|
|
2
|
+
import type { Spiceflow } from '../spiceflow.ts'
|
|
3
3
|
|
|
4
|
-
import { SpiceflowFetchError } from './errors.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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>> =
|
|
118
|
-
keyof Route,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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'>
|
package/src/client.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { createSpiceflowClient } from './client/index.
|
|
3
|
-
import { Spiceflow } from './spiceflow.
|
|
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.
|
|
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.
|
|
13
|
+
} from './types.ts'
|
|
14
14
|
|
|
15
|
-
import { SpiceflowRequest } from './spiceflow.
|
|
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.
|
|
4
|
-
import { Spiceflow } from './spiceflow.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2
|
-
export type { AnySpiceflow } from './spiceflow.
|
|
3
|
-
export { InternalServerError, ParseError, ValidationError } from './error.
|
|
1
|
+
export { Spiceflow } from './spiceflow.ts'
|
|
2
|
+
export type { AnySpiceflow } from './spiceflow.ts'
|
|
3
|
+
export { InternalServerError, ParseError, ValidationError } from './error.ts'
|
package/src/mcp-transport.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
11
|
-
import { openapi } from './openapi.
|
|
12
|
-
import { Spiceflow } from './spiceflow.
|
|
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(
|
package/src/middleware.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
-
import { Spiceflow } from './spiceflow.
|
|
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(
|
|
361
|
-
|
|
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(
|
|
365
|
-
acc
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
package/src/openapi.test.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
47
|
-
name:
|
|
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:
|
|
152
|
-
id:
|
|
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 {
|
|
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.
|
|
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)
|
|
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',
|
package/src/serialize.ts
ADDED
|
@@ -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
|
+
}
|