vector-framework 1.1.1 → 1.2.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.
- package/README.md +99 -628
- package/dist/auth/protected.d.ts +1 -0
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +3 -0
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +1 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +5 -7
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/graceful-shutdown.d.ts +15 -0
- package/dist/cli/graceful-shutdown.d.ts.map +1 -0
- package/dist/cli/graceful-shutdown.js +42 -0
- package/dist/cli/graceful-shutdown.js.map +1 -0
- package/dist/cli/index.js +46 -97
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +4 -0
- package/dist/cli/option-resolution.d.ts.map +1 -0
- package/dist/cli/option-resolution.js +28 -0
- package/dist/cli/option-resolution.js.map +1 -0
- package/dist/cli.js +3423 -660
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +7 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -17
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +432 -153
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +17 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +471 -31
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +8 -5
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +53 -14
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-generator.d.ts.map +1 -1
- package/dist/dev/route-generator.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +1 -5
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +14 -14
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +34 -41
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1420 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1420 -8
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +4 -0
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts +2 -0
- package/dist/openapi/docs-ui.d.ts.map +1 -0
- package/dist/openapi/docs-ui.js +1425 -0
- package/dist/openapi/docs-ui.js.map +1 -0
- package/dist/openapi/generator.d.ts +12 -0
- package/dist/openapi/generator.d.ts.map +1 -0
- package/dist/openapi/generator.js +502 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/start-vector.d.ts +3 -0
- package/dist/start-vector.d.ts.map +1 -0
- package/dist/start-vector.js +38 -0
- package/dist/start-vector.js.map +1 -0
- package/dist/types/index.d.ts +95 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/standard-schema.d.ts +118 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/utils/cors.d.ts +13 -0
- package/dist/utils/cors.d.ts.map +1 -0
- package/dist/utils/cors.js +89 -0
- package/dist/utils/cors.js.map +1 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/path.d.ts +6 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +5 -0
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/schema-validation.d.ts +31 -0
- package/dist/utils/schema-validation.d.ts.map +1 -0
- package/dist/utils/schema-validation.js +77 -0
- package/dist/utils/schema-validation.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +15 -12
- package/src/auth/protected.ts +7 -13
- package/src/cache/manager.ts +8 -18
- package/src/cli/graceful-shutdown.ts +60 -0
- package/src/cli/index.ts +52 -115
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +7 -4
- package/src/core/router.ts +502 -156
- package/src/core/server.ts +610 -33
- package/src/core/vector.ts +87 -33
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +2 -9
- package/src/http.ts +85 -125
- package/src/index.ts +4 -3
- package/src/middleware/manager.ts +4 -0
- package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
- package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
- package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
- package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
- package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
- package/src/openapi/assets/favicon/favicon.ico +0 -0
- package/src/openapi/assets/favicon/site.webmanifest +11 -0
- package/src/openapi/assets/logo.svg +12 -0
- package/src/openapi/assets/logo_dark.svg +6 -0
- package/src/openapi/assets/logo_icon.png +0 -0
- package/src/openapi/assets/logo_white.svg +6 -0
- package/src/openapi/assets/tailwindcdn.js +83 -0
- package/src/openapi/docs-ui.ts +1435 -0
- package/src/openapi/generator.ts +586 -0
- package/src/start-vector.ts +50 -0
- package/src/types/index.ts +138 -17
- package/src/types/standard-schema.ts +147 -0
- package/src/utils/cors.ts +101 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/path.ts +6 -0
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +3 -0
package/src/core/server.ts
CHANGED
|
@@ -1,28 +1,270 @@
|
|
|
1
1
|
import type { Server } from 'bun';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { STATIC_RESPONSES } from '../constants';
|
|
5
|
+
import { cors } from '../utils/cors';
|
|
6
|
+
import { renderOpenAPIDocsHtml } from '../openapi/docs-ui';
|
|
7
|
+
import { generateOpenAPIDocument } from '../openapi/generator';
|
|
8
|
+
import type { CorsOptions, DefaultVectorTypes, OpenAPIOptions, VectorConfig, VectorTypes } from '../types';
|
|
4
9
|
import type { VectorRouter } from './router';
|
|
5
10
|
|
|
11
|
+
interface NormalizedOpenAPIConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
path: string;
|
|
14
|
+
target: string;
|
|
15
|
+
docs: {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
path: string;
|
|
18
|
+
exposePaths?: string[];
|
|
19
|
+
};
|
|
20
|
+
info?: {
|
|
21
|
+
title?: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const OPENAPI_TAILWIND_ASSET_PATH = '/_vector/openapi/tailwindcdn.js';
|
|
28
|
+
const OPENAPI_LOGO_DARK_ASSET_PATH = '/_vector/openapi/logo_dark.svg';
|
|
29
|
+
const OPENAPI_LOGO_WHITE_ASSET_PATH = '/_vector/openapi/logo_white.svg';
|
|
30
|
+
const OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH = '/_vector/openapi/favicon/apple-touch-icon.png';
|
|
31
|
+
const OPENAPI_FAVICON_32_ASSET_PATH = '/_vector/openapi/favicon/favicon-32x32.png';
|
|
32
|
+
const OPENAPI_FAVICON_16_ASSET_PATH = '/_vector/openapi/favicon/favicon-16x16.png';
|
|
33
|
+
const OPENAPI_FAVICON_ICO_ASSET_PATH = '/_vector/openapi/favicon/favicon.ico';
|
|
34
|
+
const OPENAPI_WEBMANIFEST_ASSET_PATH = '/_vector/openapi/favicon/site.webmanifest';
|
|
35
|
+
const OPENAPI_ANDROID_192_ASSET_PATH = '/_vector/openapi/favicon/android-chrome-192x192.png';
|
|
36
|
+
const OPENAPI_ANDROID_512_ASSET_PATH = '/_vector/openapi/favicon/android-chrome-512x512.png';
|
|
37
|
+
const OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
|
|
38
|
+
// Source execution (src/core/server.ts -> src/openapi/assets/tailwindcdn.js)
|
|
39
|
+
'../openapi/assets/tailwindcdn.js',
|
|
40
|
+
// Bundled dist entrypoints (dist/index.mjs|dist/cli.js -> src/openapi/assets/tailwindcdn.js)
|
|
41
|
+
'../src/openapi/assets/tailwindcdn.js',
|
|
42
|
+
// Unbundled dist/core/server.js execution (dist/core -> src/openapi/assets/tailwindcdn.js)
|
|
43
|
+
'../../src/openapi/assets/tailwindcdn.js',
|
|
44
|
+
] as const;
|
|
45
|
+
const OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES = [
|
|
46
|
+
// Source execution (src/core/server.ts -> src/openapi/assets/logo_dark.svg)
|
|
47
|
+
'../openapi/assets/logo_dark.svg',
|
|
48
|
+
// Bundled dist entrypoints (dist/index.mjs|dist/cli.js -> src/openapi/assets/logo_dark.svg)
|
|
49
|
+
'../src/openapi/assets/logo_dark.svg',
|
|
50
|
+
// Unbundled dist/core/server.js execution (dist/core -> src/openapi/assets/logo_dark.svg)
|
|
51
|
+
'../../src/openapi/assets/logo_dark.svg',
|
|
52
|
+
] as const;
|
|
53
|
+
const OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES = [
|
|
54
|
+
// Source execution (src/core/server.ts -> src/openapi/assets/logo_white.svg)
|
|
55
|
+
'../openapi/assets/logo_white.svg',
|
|
56
|
+
// Bundled dist entrypoints (dist/index.mjs|dist/cli.js -> src/openapi/assets/logo_white.svg)
|
|
57
|
+
'../src/openapi/assets/logo_white.svg',
|
|
58
|
+
// Unbundled dist/core/server.js execution (dist/core -> src/openapi/assets/logo_white.svg)
|
|
59
|
+
'../../src/openapi/assets/logo_white.svg',
|
|
60
|
+
] as const;
|
|
61
|
+
const OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
|
|
62
|
+
'src/openapi/assets/tailwindcdn.js',
|
|
63
|
+
'openapi/assets/tailwindcdn.js',
|
|
64
|
+
'dist/openapi/assets/tailwindcdn.js',
|
|
65
|
+
] as const;
|
|
66
|
+
const OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES = [
|
|
67
|
+
'src/openapi/assets/logo_dark.svg',
|
|
68
|
+
'openapi/assets/logo_dark.svg',
|
|
69
|
+
'dist/openapi/assets/logo_dark.svg',
|
|
70
|
+
] as const;
|
|
71
|
+
const OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES = [
|
|
72
|
+
'src/openapi/assets/logo_white.svg',
|
|
73
|
+
'openapi/assets/logo_white.svg',
|
|
74
|
+
'dist/openapi/assets/logo_white.svg',
|
|
75
|
+
] as const;
|
|
76
|
+
const OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES = [
|
|
77
|
+
'../openapi/assets/favicon',
|
|
78
|
+
'../src/openapi/assets/favicon',
|
|
79
|
+
'../../src/openapi/assets/favicon',
|
|
80
|
+
] as const;
|
|
81
|
+
const OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES = [
|
|
82
|
+
'src/openapi/assets/favicon',
|
|
83
|
+
'openapi/assets/favicon',
|
|
84
|
+
'dist/openapi/assets/favicon',
|
|
85
|
+
] as const;
|
|
86
|
+
const OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK = '/* OpenAPI docs runtime asset missing: tailwind disabled */';
|
|
87
|
+
|
|
88
|
+
function buildOpenAPIAssetCandidatePaths(bases: readonly string[], filename: string): string[] {
|
|
89
|
+
return bases.map((base) => `${base}/${filename}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveOpenAPIAssetFile(
|
|
93
|
+
relativeCandidates: readonly string[],
|
|
94
|
+
cwdCandidates: readonly string[]
|
|
95
|
+
): ReturnType<typeof Bun.file> | null {
|
|
96
|
+
for (const relativePath of relativeCandidates) {
|
|
97
|
+
try {
|
|
98
|
+
const fileUrl = new URL(relativePath, import.meta.url);
|
|
99
|
+
if (existsSync(fileUrl)) {
|
|
100
|
+
return Bun.file(fileUrl);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore resolution failures and try the next candidate.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cwd = process.cwd();
|
|
108
|
+
for (const relativePath of cwdCandidates) {
|
|
109
|
+
const absolutePath = join(cwd, relativePath);
|
|
110
|
+
if (existsSync(absolutePath)) {
|
|
111
|
+
return Bun.file(absolutePath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
119
|
+
OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES,
|
|
120
|
+
OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES
|
|
121
|
+
);
|
|
122
|
+
const OPENAPI_LOGO_DARK_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
123
|
+
OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES,
|
|
124
|
+
OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES
|
|
125
|
+
);
|
|
126
|
+
const OPENAPI_LOGO_WHITE_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
127
|
+
OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES,
|
|
128
|
+
OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES
|
|
129
|
+
);
|
|
130
|
+
const OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
131
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'apple-touch-icon.png'),
|
|
132
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'apple-touch-icon.png')
|
|
133
|
+
);
|
|
134
|
+
const OPENAPI_FAVICON_32_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
135
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'favicon-32x32.png'),
|
|
136
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'favicon-32x32.png')
|
|
137
|
+
);
|
|
138
|
+
const OPENAPI_FAVICON_16_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
139
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'favicon-16x16.png'),
|
|
140
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'favicon-16x16.png')
|
|
141
|
+
);
|
|
142
|
+
const OPENAPI_FAVICON_ICO_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
143
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'favicon.ico'),
|
|
144
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'favicon.ico')
|
|
145
|
+
);
|
|
146
|
+
const OPENAPI_WEBMANIFEST_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
147
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'site.webmanifest'),
|
|
148
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'site.webmanifest')
|
|
149
|
+
);
|
|
150
|
+
const OPENAPI_ANDROID_192_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
151
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'android-chrome-192x192.png'),
|
|
152
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'android-chrome-192x192.png')
|
|
153
|
+
);
|
|
154
|
+
const OPENAPI_ANDROID_512_ASSET_FILE = resolveOpenAPIAssetFile(
|
|
155
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, 'android-chrome-512x512.png'),
|
|
156
|
+
buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, 'android-chrome-512x512.png')
|
|
157
|
+
);
|
|
158
|
+
const OPENAPI_FAVICON_ASSETS = [
|
|
159
|
+
{
|
|
160
|
+
path: OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
|
|
161
|
+
file: OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE,
|
|
162
|
+
contentType: 'image/png',
|
|
163
|
+
filename: 'apple-touch-icon.png',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
path: OPENAPI_FAVICON_32_ASSET_PATH,
|
|
167
|
+
file: OPENAPI_FAVICON_32_ASSET_FILE,
|
|
168
|
+
contentType: 'image/png',
|
|
169
|
+
filename: 'favicon-32x32.png',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
path: OPENAPI_FAVICON_16_ASSET_PATH,
|
|
173
|
+
file: OPENAPI_FAVICON_16_ASSET_FILE,
|
|
174
|
+
contentType: 'image/png',
|
|
175
|
+
filename: 'favicon-16x16.png',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
path: OPENAPI_FAVICON_ICO_ASSET_PATH,
|
|
179
|
+
file: OPENAPI_FAVICON_ICO_ASSET_FILE,
|
|
180
|
+
contentType: 'image/x-icon',
|
|
181
|
+
filename: 'favicon.ico',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: OPENAPI_WEBMANIFEST_ASSET_PATH,
|
|
185
|
+
file: OPENAPI_WEBMANIFEST_ASSET_FILE,
|
|
186
|
+
contentType: 'application/manifest+json; charset=utf-8',
|
|
187
|
+
filename: 'site.webmanifest',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: OPENAPI_ANDROID_192_ASSET_PATH,
|
|
191
|
+
file: OPENAPI_ANDROID_192_ASSET_FILE,
|
|
192
|
+
contentType: 'image/png',
|
|
193
|
+
filename: 'android-chrome-192x192.png',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: OPENAPI_ANDROID_512_ASSET_PATH,
|
|
197
|
+
file: OPENAPI_ANDROID_512_ASSET_FILE,
|
|
198
|
+
contentType: 'image/png',
|
|
199
|
+
filename: 'android-chrome-512x512.png',
|
|
200
|
+
},
|
|
201
|
+
] as const;
|
|
202
|
+
const DOCS_HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
|
203
|
+
const DOCS_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
204
|
+
const DOCS_ASSET_ERROR_CACHE_CONTROL = 'no-store';
|
|
205
|
+
|
|
206
|
+
interface OpenAPIDocsHtmlCacheEntry {
|
|
207
|
+
html: string;
|
|
208
|
+
gzip: Uint8Array;
|
|
209
|
+
etag: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function escapeRegex(value: string): string {
|
|
213
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function wildcardPatternToRegex(pattern: string): RegExp {
|
|
217
|
+
let regexSource = '^';
|
|
218
|
+
for (const char of pattern) {
|
|
219
|
+
if (char === '*') {
|
|
220
|
+
regexSource += '.*';
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
regexSource += escapeRegex(char);
|
|
224
|
+
}
|
|
225
|
+
regexSource += '$';
|
|
226
|
+
return new RegExp(regexSource);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function matchesExposePath(path: string, exposePathPattern: string): boolean {
|
|
230
|
+
if (!exposePathPattern.includes('*')) {
|
|
231
|
+
return path === exposePathPattern;
|
|
232
|
+
}
|
|
233
|
+
return wildcardPatternToRegex(exposePathPattern).test(path);
|
|
234
|
+
}
|
|
235
|
+
|
|
6
236
|
export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
7
237
|
private server: Server | null = null;
|
|
8
238
|
private router: VectorRouter<TTypes>;
|
|
9
239
|
private config: VectorConfig<TTypes>;
|
|
10
|
-
private
|
|
11
|
-
private
|
|
240
|
+
private openapiConfig: NormalizedOpenAPIConfig;
|
|
241
|
+
private openapiDocCache: Record<string, unknown> | null = null;
|
|
242
|
+
private openapiDocsHtmlCache: OpenAPIDocsHtmlCacheEntry | null = null;
|
|
243
|
+
private openapiWarningsLogged = false;
|
|
244
|
+
private openapiTailwindMissingLogged = false;
|
|
245
|
+
private openapiLogoDarkMissingLogged = false;
|
|
246
|
+
private openapiLogoWhiteMissingLogged = false;
|
|
247
|
+
private corsHandler: {
|
|
248
|
+
preflight: (request: Request) => Response;
|
|
249
|
+
corsify: (response: Response, request: Request) => Response;
|
|
250
|
+
} | null = null;
|
|
251
|
+
private corsHeadersEntries: [string, string][] | null = null;
|
|
12
252
|
|
|
13
253
|
constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
|
|
14
254
|
this.router = router;
|
|
15
255
|
this.config = config;
|
|
256
|
+
this.openapiConfig = this.normalizeOpenAPIConfig(config.openapi, config.development);
|
|
16
257
|
|
|
17
258
|
if (config.cors) {
|
|
18
259
|
const opts = this.normalizeCorsOptions(config.cors);
|
|
19
260
|
const { preflight, corsify } = cors(opts);
|
|
20
261
|
this.corsHandler = { preflight, corsify };
|
|
21
262
|
|
|
22
|
-
// Pre-build static CORS headers when origin
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
263
|
+
// Pre-build static CORS headers when origin does not require per-request reflection.
|
|
264
|
+
const canUseStaticCorsHeaders = typeof opts.origin === 'string' && (opts.origin !== '*' || !opts.credentials);
|
|
265
|
+
|
|
266
|
+
if (canUseStaticCorsHeaders) {
|
|
267
|
+
const corsHeaders: Record<string, string> = {
|
|
26
268
|
'access-control-allow-origin': opts.origin,
|
|
27
269
|
'access-control-allow-methods': opts.allowMethods,
|
|
28
270
|
'access-control-allow-headers': opts.allowHeaders,
|
|
@@ -30,10 +272,333 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
30
272
|
'access-control-max-age': String(opts.maxAge),
|
|
31
273
|
};
|
|
32
274
|
if (opts.credentials) {
|
|
33
|
-
|
|
275
|
+
corsHeaders['access-control-allow-credentials'] = 'true';
|
|
276
|
+
}
|
|
277
|
+
this.corsHeadersEntries = Object.entries(corsHeaders);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pass CORS behavior to router so matched routes also receive CORS headers.
|
|
281
|
+
this.router.setCorsHeaders(this.corsHeadersEntries);
|
|
282
|
+
this.router.setCorsHandler(this.corsHeadersEntries ? null : this.corsHandler.corsify);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private normalizeOpenAPIConfig(
|
|
287
|
+
openapi: OpenAPIOptions | boolean | undefined,
|
|
288
|
+
development: boolean | undefined
|
|
289
|
+
): NormalizedOpenAPIConfig {
|
|
290
|
+
const isDev = development !== false && process.env.NODE_ENV !== 'production';
|
|
291
|
+
const defaultEnabled = isDev;
|
|
292
|
+
|
|
293
|
+
if (openapi === false) {
|
|
294
|
+
return {
|
|
295
|
+
enabled: false,
|
|
296
|
+
path: '/openapi.json',
|
|
297
|
+
target: 'openapi-3.0',
|
|
298
|
+
docs: { enabled: false, path: '/docs' },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (openapi === true) {
|
|
303
|
+
return {
|
|
304
|
+
enabled: true,
|
|
305
|
+
path: '/openapi.json',
|
|
306
|
+
target: 'openapi-3.0',
|
|
307
|
+
docs: { enabled: false, path: '/docs' },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const openapiObject = openapi || {};
|
|
312
|
+
const docsValue = openapiObject.docs;
|
|
313
|
+
const docs =
|
|
314
|
+
typeof docsValue === 'boolean'
|
|
315
|
+
? { enabled: docsValue, path: '/docs', exposePaths: undefined }
|
|
316
|
+
: {
|
|
317
|
+
enabled: docsValue?.enabled === true,
|
|
318
|
+
path: docsValue?.path || '/docs',
|
|
319
|
+
exposePaths: Array.isArray(docsValue?.exposePaths)
|
|
320
|
+
? docsValue.exposePaths
|
|
321
|
+
.map((path) => (typeof path === 'string' ? path.trim() : ''))
|
|
322
|
+
.filter((path) => path.length > 0)
|
|
323
|
+
: undefined,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
enabled: openapiObject.enabled ?? defaultEnabled,
|
|
328
|
+
path: openapiObject.path || '/openapi.json',
|
|
329
|
+
target: openapiObject.target || 'openapi-3.0',
|
|
330
|
+
docs,
|
|
331
|
+
info: openapiObject.info,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private isDocsReservedPath(path: string): boolean {
|
|
336
|
+
return (
|
|
337
|
+
path === this.openapiConfig.path || (this.openapiConfig.docs.enabled && path === this.openapiConfig.docs.path)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private getOpenAPIDocument(): Record<string, unknown> {
|
|
342
|
+
if (this.openapiDocCache) {
|
|
343
|
+
return this.openapiDocCache;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const routes = this.router.getRouteDefinitions().filter((route) => !this.isDocsReservedPath(route.path));
|
|
347
|
+
|
|
348
|
+
const result = generateOpenAPIDocument(routes as any, {
|
|
349
|
+
target: this.openapiConfig.target,
|
|
350
|
+
info: this.openapiConfig.info,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!this.openapiWarningsLogged && result.warnings.length > 0) {
|
|
354
|
+
for (const warning of result.warnings) {
|
|
355
|
+
console.warn(warning);
|
|
356
|
+
}
|
|
357
|
+
this.openapiWarningsLogged = true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.openapiDocCache = result.document;
|
|
361
|
+
return this.openapiDocCache;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getOpenAPIDocumentForDocs(): Record<string, unknown> {
|
|
365
|
+
const exposePaths = this.openapiConfig.docs.exposePaths;
|
|
366
|
+
const document = this.getOpenAPIDocument();
|
|
367
|
+
|
|
368
|
+
if (!Array.isArray(exposePaths) || exposePaths.length === 0) {
|
|
369
|
+
return document;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const existingPaths =
|
|
373
|
+
document.paths && typeof document.paths === 'object' && !Array.isArray(document.paths)
|
|
374
|
+
? (document.paths as Record<string, unknown>)
|
|
375
|
+
: {};
|
|
376
|
+
|
|
377
|
+
const filteredPaths: Record<string, unknown> = {};
|
|
378
|
+
for (const [path, value] of Object.entries(existingPaths)) {
|
|
379
|
+
if (exposePaths.some((pattern) => matchesExposePath(path, pattern))) {
|
|
380
|
+
filteredPaths[path] = value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
...document,
|
|
386
|
+
paths: filteredPaths,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private getOpenAPIDocsHtmlCacheEntry(): OpenAPIDocsHtmlCacheEntry {
|
|
391
|
+
if (this.openapiDocsHtmlCache) {
|
|
392
|
+
return this.openapiDocsHtmlCache;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const html = renderOpenAPIDocsHtml(
|
|
396
|
+
this.getOpenAPIDocumentForDocs(),
|
|
397
|
+
this.openapiConfig.path,
|
|
398
|
+
OPENAPI_TAILWIND_ASSET_PATH,
|
|
399
|
+
OPENAPI_LOGO_DARK_ASSET_PATH,
|
|
400
|
+
OPENAPI_LOGO_WHITE_ASSET_PATH,
|
|
401
|
+
OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
|
|
402
|
+
OPENAPI_FAVICON_32_ASSET_PATH,
|
|
403
|
+
OPENAPI_FAVICON_16_ASSET_PATH,
|
|
404
|
+
OPENAPI_WEBMANIFEST_ASSET_PATH
|
|
405
|
+
);
|
|
406
|
+
const gzip = Bun.gzipSync(html);
|
|
407
|
+
const etag = `"${Bun.hash(html).toString(16)}"`;
|
|
408
|
+
|
|
409
|
+
this.openapiDocsHtmlCache = { html, gzip, etag };
|
|
410
|
+
return this.openapiDocsHtmlCache;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private requestAcceptsGzip(request: Request): boolean {
|
|
414
|
+
const acceptEncoding = request.headers.get('accept-encoding');
|
|
415
|
+
return Boolean(acceptEncoding && /\bgzip\b/i.test(acceptEncoding));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private validateReservedOpenAPIPaths(): void {
|
|
419
|
+
if (!this.openapiConfig.enabled) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const reserved = new Set<string>([this.openapiConfig.path]);
|
|
424
|
+
if (this.openapiConfig.docs.enabled) {
|
|
425
|
+
reserved.add(this.openapiConfig.docs.path);
|
|
426
|
+
reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
|
|
427
|
+
reserved.add(OPENAPI_LOGO_DARK_ASSET_PATH);
|
|
428
|
+
reserved.add(OPENAPI_LOGO_WHITE_ASSET_PATH);
|
|
429
|
+
for (const asset of OPENAPI_FAVICON_ASSETS) {
|
|
430
|
+
reserved.add(asset.path);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const methodConflicts = this.router
|
|
435
|
+
.getRouteDefinitions()
|
|
436
|
+
.filter((route) => reserved.has(route.path))
|
|
437
|
+
.map((route) => `${route.method} ${route.path}`);
|
|
438
|
+
|
|
439
|
+
const staticConflicts = Object.entries(this.router.getRouteTable())
|
|
440
|
+
.filter(([path, value]) => reserved.has(path) && value instanceof Response)
|
|
441
|
+
.map(([path]) => `STATIC ${path}`);
|
|
442
|
+
|
|
443
|
+
const conflicts = [...methodConflicts, ...staticConflicts];
|
|
444
|
+
|
|
445
|
+
if (conflicts.length > 0) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`OpenAPI reserved path conflict: ${conflicts.join(
|
|
448
|
+
', '
|
|
449
|
+
)}. Change your route path(s) or reconfigure openapi.path/docs.path.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private tryHandleOpenAPIRequest(request: Request): Response | null {
|
|
455
|
+
if (!this.openapiConfig.enabled || request.method !== 'GET') {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const pathname = new URL(request.url).pathname;
|
|
460
|
+
if (pathname === this.openapiConfig.path) {
|
|
461
|
+
return Response.json(this.getOpenAPIDocument());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (this.openapiConfig.docs.enabled && pathname === this.openapiConfig.docs.path) {
|
|
465
|
+
const { html, gzip, etag } = this.getOpenAPIDocsHtmlCacheEntry();
|
|
466
|
+
if (request.headers.get('if-none-match') === etag) {
|
|
467
|
+
return new Response(null, {
|
|
468
|
+
status: 304,
|
|
469
|
+
headers: {
|
|
470
|
+
etag,
|
|
471
|
+
'cache-control': DOCS_HTML_CACHE_CONTROL,
|
|
472
|
+
vary: 'accept-encoding',
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (this.requestAcceptsGzip(request)) {
|
|
478
|
+
return new Response(gzip, {
|
|
479
|
+
status: 200,
|
|
480
|
+
headers: {
|
|
481
|
+
'content-type': 'text/html; charset=utf-8',
|
|
482
|
+
'content-encoding': 'gzip',
|
|
483
|
+
etag,
|
|
484
|
+
'cache-control': DOCS_HTML_CACHE_CONTROL,
|
|
485
|
+
vary: 'accept-encoding',
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return new Response(html, {
|
|
491
|
+
status: 200,
|
|
492
|
+
headers: {
|
|
493
|
+
'content-type': 'text/html; charset=utf-8',
|
|
494
|
+
etag,
|
|
495
|
+
'cache-control': DOCS_HTML_CACHE_CONTROL,
|
|
496
|
+
vary: 'accept-encoding',
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_TAILWIND_ASSET_PATH) {
|
|
502
|
+
if (!OPENAPI_TAILWIND_ASSET_FILE) {
|
|
503
|
+
if (!this.openapiTailwindMissingLogged) {
|
|
504
|
+
this.openapiTailwindMissingLogged = true;
|
|
505
|
+
console.warn(
|
|
506
|
+
'[OpenAPI] Missing docs runtime asset "tailwindcdn.js". Serving inline fallback script instead.'
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return new Response(OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK, {
|
|
511
|
+
status: 200,
|
|
512
|
+
headers: {
|
|
513
|
+
'content-type': 'application/javascript; charset=utf-8',
|
|
514
|
+
'cache-control': DOCS_ASSET_CACHE_CONTROL,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return new Response(OPENAPI_TAILWIND_ASSET_FILE, {
|
|
520
|
+
status: 200,
|
|
521
|
+
headers: {
|
|
522
|
+
'content-type': 'application/javascript; charset=utf-8',
|
|
523
|
+
'cache-control': DOCS_ASSET_CACHE_CONTROL,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_DARK_ASSET_PATH) {
|
|
529
|
+
if (!OPENAPI_LOGO_DARK_ASSET_FILE) {
|
|
530
|
+
if (!this.openapiLogoDarkMissingLogged) {
|
|
531
|
+
this.openapiLogoDarkMissingLogged = true;
|
|
532
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_dark.svg".');
|
|
34
533
|
}
|
|
534
|
+
|
|
535
|
+
return new Response('OpenAPI docs runtime asset missing: logo_dark.svg', {
|
|
536
|
+
status: 404,
|
|
537
|
+
headers: {
|
|
538
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
539
|
+
'cache-control': DOCS_ASSET_ERROR_CACHE_CONTROL,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
35
542
|
}
|
|
543
|
+
|
|
544
|
+
return new Response(OPENAPI_LOGO_DARK_ASSET_FILE, {
|
|
545
|
+
status: 200,
|
|
546
|
+
headers: {
|
|
547
|
+
'content-type': 'image/svg+xml; charset=utf-8',
|
|
548
|
+
'cache-control': DOCS_ASSET_CACHE_CONTROL,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_WHITE_ASSET_PATH) {
|
|
554
|
+
if (!OPENAPI_LOGO_WHITE_ASSET_FILE) {
|
|
555
|
+
if (!this.openapiLogoWhiteMissingLogged) {
|
|
556
|
+
this.openapiLogoWhiteMissingLogged = true;
|
|
557
|
+
console.warn('[OpenAPI] Missing docs runtime asset "logo_white.svg".');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return new Response('OpenAPI docs runtime asset missing: logo_white.svg', {
|
|
561
|
+
status: 404,
|
|
562
|
+
headers: {
|
|
563
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
564
|
+
'cache-control': DOCS_ASSET_ERROR_CACHE_CONTROL,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return new Response(OPENAPI_LOGO_WHITE_ASSET_FILE, {
|
|
570
|
+
status: 200,
|
|
571
|
+
headers: {
|
|
572
|
+
'content-type': 'image/svg+xml; charset=utf-8',
|
|
573
|
+
'cache-control': DOCS_ASSET_CACHE_CONTROL,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
36
576
|
}
|
|
577
|
+
|
|
578
|
+
if (this.openapiConfig.docs.enabled) {
|
|
579
|
+
const faviconAsset = OPENAPI_FAVICON_ASSETS.find((asset) => asset.path === pathname);
|
|
580
|
+
if (faviconAsset) {
|
|
581
|
+
if (!faviconAsset.file) {
|
|
582
|
+
return new Response(`OpenAPI docs runtime asset missing: ${faviconAsset.filename}`, {
|
|
583
|
+
status: 404,
|
|
584
|
+
headers: {
|
|
585
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
586
|
+
'cache-control': DOCS_ASSET_ERROR_CACHE_CONTROL,
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return new Response(faviconAsset.file, {
|
|
592
|
+
status: 200,
|
|
593
|
+
headers: {
|
|
594
|
+
'content-type': faviconAsset.contentType,
|
|
595
|
+
'cache-control': DOCS_ASSET_CACHE_CONTROL,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return null;
|
|
37
602
|
}
|
|
38
603
|
|
|
39
604
|
private normalizeCorsOptions(options: CorsOptions): any {
|
|
@@ -53,33 +618,45 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
53
618
|
};
|
|
54
619
|
}
|
|
55
620
|
|
|
621
|
+
private applyCors(response: Response, request?: Request): Response {
|
|
622
|
+
if (this.corsHeadersEntries) {
|
|
623
|
+
for (const [k, v] of this.corsHeadersEntries) {
|
|
624
|
+
response.headers.set(k, v);
|
|
625
|
+
}
|
|
626
|
+
return response;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (this.corsHandler && request) {
|
|
630
|
+
return this.corsHandler.corsify(response, request);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return response;
|
|
634
|
+
}
|
|
635
|
+
|
|
56
636
|
async start(): Promise<Server> {
|
|
57
|
-
const port = this.config.port
|
|
637
|
+
const port = this.config.port ?? 3000;
|
|
58
638
|
const hostname = this.config.hostname || 'localhost';
|
|
59
639
|
|
|
60
|
-
|
|
640
|
+
this.validateReservedOpenAPIPaths();
|
|
641
|
+
|
|
642
|
+
const fallbackFetch = async (request: Request): Promise<Response> => {
|
|
61
643
|
try {
|
|
62
|
-
// Handle CORS preflight
|
|
644
|
+
// Handle CORS preflight for any path
|
|
63
645
|
if (this.corsHandler && request.method === 'OPTIONS') {
|
|
64
646
|
return this.corsHandler.preflight(request);
|
|
65
647
|
}
|
|
66
648
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (this.corsHeaders) {
|
|
72
|
-
for (const [k, v] of Object.entries(this.corsHeaders)) {
|
|
73
|
-
response.headers.set(k, v);
|
|
74
|
-
}
|
|
75
|
-
} else if (this.corsHandler) {
|
|
76
|
-
response = this.corsHandler.corsify(response, request);
|
|
649
|
+
// Handle built-in docs endpoints for requests that fell through the Bun route table.
|
|
650
|
+
const openapiResponse = this.tryHandleOpenAPIRequest(request);
|
|
651
|
+
if (openapiResponse) {
|
|
652
|
+
return this.applyCors(openapiResponse, request);
|
|
77
653
|
}
|
|
78
654
|
|
|
79
|
-
return
|
|
655
|
+
// No route matched — return 404
|
|
656
|
+
return this.applyCors(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
|
|
80
657
|
} catch (error) {
|
|
81
658
|
console.error('Server error:', error);
|
|
82
|
-
return new Response('Internal Server Error', { status: 500 });
|
|
659
|
+
return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
|
|
83
660
|
}
|
|
84
661
|
};
|
|
85
662
|
|
|
@@ -88,24 +665,21 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
88
665
|
port,
|
|
89
666
|
hostname,
|
|
90
667
|
reusePort: this.config.reusePort !== false,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
668
|
+
routes: this.router.getRouteTable(),
|
|
669
|
+
fetch: fallbackFetch,
|
|
670
|
+
idleTimeout: this.config.idleTimeout ?? 60,
|
|
671
|
+
error: (error, request?: Request) => {
|
|
94
672
|
console.error('[ERROR] Server error:', error);
|
|
95
|
-
return new Response('Internal Server Error', { status: 500 });
|
|
673
|
+
return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
|
|
96
674
|
},
|
|
97
675
|
});
|
|
98
676
|
|
|
99
|
-
// Validate that the server actually started
|
|
100
677
|
if (!this.server || !this.server.port) {
|
|
101
678
|
throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
|
|
102
679
|
}
|
|
103
680
|
|
|
104
|
-
// Server logs are handled by CLI
|
|
105
|
-
|
|
106
681
|
return this.server;
|
|
107
682
|
} catch (error: any) {
|
|
108
|
-
// Enhance error message with context for common issues
|
|
109
683
|
if (error.code === 'EADDRINUSE' || error.message?.includes('address already in use')) {
|
|
110
684
|
error.message = `Port ${port} is already in use`;
|
|
111
685
|
error.port = port;
|
|
@@ -125,6 +699,9 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
125
699
|
if (this.server) {
|
|
126
700
|
this.server.stop();
|
|
127
701
|
this.server = null;
|
|
702
|
+
this.openapiDocCache = null;
|
|
703
|
+
this.openapiDocsHtmlCache = null;
|
|
704
|
+
this.openapiWarningsLogged = false;
|
|
128
705
|
console.log('Server stopped');
|
|
129
706
|
}
|
|
130
707
|
}
|
|
@@ -134,7 +711,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
134
711
|
}
|
|
135
712
|
|
|
136
713
|
getPort(): number {
|
|
137
|
-
return this.server?.port
|
|
714
|
+
return this.server?.port ?? this.config.port ?? 3000;
|
|
138
715
|
}
|
|
139
716
|
|
|
140
717
|
getHostname(): string {
|