vector-framework 1.2.2 → 1.2.3
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 +18 -6
- package/dist/auth/protected.d.ts +4 -4
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +10 -7
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +2 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -4
- package/dist/cache/manager.js.map +1 -1
- package/dist/checkpoint/artifacts/compressor.d.ts +5 -0
- package/dist/checkpoint/artifacts/compressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/compressor.js +24 -0
- package/dist/checkpoint/artifacts/compressor.js.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts +2 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.js +31 -0
- package/dist/checkpoint/artifacts/decompress-worker.js.map +1 -0
- package/dist/checkpoint/artifacts/hasher.d.ts +2 -0
- package/dist/checkpoint/artifacts/hasher.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/hasher.js +7 -0
- package/dist/checkpoint/artifacts/hasher.js.map +1 -0
- package/dist/checkpoint/artifacts/manifest.d.ts +6 -0
- package/dist/checkpoint/artifacts/manifest.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/manifest.js +55 -0
- package/dist/checkpoint/artifacts/manifest.js.map +1 -0
- package/dist/checkpoint/artifacts/materializer.d.ts +16 -0
- package/dist/checkpoint/artifacts/materializer.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/materializer.js +168 -0
- package/dist/checkpoint/artifacts/materializer.js.map +1 -0
- package/dist/checkpoint/artifacts/packager.d.ts +12 -0
- package/dist/checkpoint/artifacts/packager.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/packager.js +82 -0
- package/dist/checkpoint/artifacts/packager.js.map +1 -0
- package/dist/checkpoint/artifacts/repository.d.ts +11 -0
- package/dist/checkpoint/artifacts/repository.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/repository.js +29 -0
- package/dist/checkpoint/artifacts/repository.js.map +1 -0
- package/dist/checkpoint/artifacts/store.d.ts +13 -0
- package/dist/checkpoint/artifacts/store.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/store.js +85 -0
- package/dist/checkpoint/artifacts/store.js.map +1 -0
- package/dist/checkpoint/artifacts/types.d.ts +21 -0
- package/dist/checkpoint/artifacts/types.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/types.js +2 -0
- package/dist/checkpoint/artifacts/types.js.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts +17 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js +148 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js.map +1 -0
- package/dist/checkpoint/asset-store.d.ts +10 -0
- package/dist/checkpoint/asset-store.d.ts.map +1 -0
- package/dist/checkpoint/asset-store.js +46 -0
- package/dist/checkpoint/asset-store.js.map +1 -0
- package/dist/checkpoint/bundler.d.ts +15 -0
- package/dist/checkpoint/bundler.d.ts.map +1 -0
- package/dist/checkpoint/bundler.js +45 -0
- package/dist/checkpoint/bundler.js.map +1 -0
- package/dist/checkpoint/cli.d.ts +2 -0
- package/dist/checkpoint/cli.d.ts.map +1 -0
- package/dist/checkpoint/cli.js +157 -0
- package/dist/checkpoint/cli.js.map +1 -0
- package/dist/checkpoint/entrypoint-generator.d.ts +17 -0
- package/dist/checkpoint/entrypoint-generator.d.ts.map +1 -0
- package/dist/checkpoint/entrypoint-generator.js +251 -0
- package/dist/checkpoint/entrypoint-generator.js.map +1 -0
- package/dist/checkpoint/forwarder.d.ts +6 -0
- package/dist/checkpoint/forwarder.d.ts.map +1 -0
- package/dist/checkpoint/forwarder.js +74 -0
- package/dist/checkpoint/forwarder.js.map +1 -0
- package/dist/checkpoint/gateway.d.ts +11 -0
- package/dist/checkpoint/gateway.d.ts.map +1 -0
- package/dist/checkpoint/gateway.js +30 -0
- package/dist/checkpoint/gateway.js.map +1 -0
- package/dist/checkpoint/ipc.d.ts +12 -0
- package/dist/checkpoint/ipc.d.ts.map +1 -0
- package/dist/checkpoint/ipc.js +96 -0
- package/dist/checkpoint/ipc.js.map +1 -0
- package/dist/checkpoint/manager.d.ts +20 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +214 -0
- package/dist/checkpoint/manager.js.map +1 -0
- package/dist/checkpoint/process-manager.d.ts +35 -0
- package/dist/checkpoint/process-manager.d.ts.map +1 -0
- package/dist/checkpoint/process-manager.js +203 -0
- package/dist/checkpoint/process-manager.js.map +1 -0
- package/dist/checkpoint/resolver.d.ts +25 -0
- package/dist/checkpoint/resolver.d.ts.map +1 -0
- package/dist/checkpoint/resolver.js +95 -0
- package/dist/checkpoint/resolver.js.map +1 -0
- package/dist/checkpoint/socket-path.d.ts +2 -0
- package/dist/checkpoint/socket-path.d.ts.map +1 -0
- package/dist/checkpoint/socket-path.js +51 -0
- package/dist/checkpoint/socket-path.js.map +1 -0
- package/dist/checkpoint/types.d.ts +54 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +2 -0
- package/dist/checkpoint/types.js.map +1 -0
- package/dist/cli/index.js +10 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +1 -1
- package/dist/cli/option-resolution.d.ts.map +1 -1
- package/dist/cli/option-resolution.js.map +1 -1
- package/dist/cli.js +3709 -328
- package/dist/core/config-loader.d.ts +1 -0
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +10 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +24 -3
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +398 -249
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +2 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +22 -8
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +3 -0
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +51 -1
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +2 -1
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +32 -7
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +144 -13
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +1297 -74
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1296 -73
- package/dist/middleware/manager.d.ts +3 -3
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +9 -8
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts.map +1 -1
- package/dist/openapi/docs-ui.js +1097 -61
- package/dist/openapi/docs-ui.js.map +1 -1
- package/dist/openapi/generator.d.ts +2 -1
- package/dist/openapi/generator.d.ts.map +1 -1
- package/dist/openapi/generator.js +240 -7
- package/dist/openapi/generator.js.map +1 -1
- package/dist/types/index.d.ts +71 -28
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +24 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -2
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -1
- package/src/auth/protected.ts +11 -8
- package/src/cache/manager.ts +23 -4
- package/src/checkpoint/artifacts/compressor.ts +30 -0
- package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
- package/src/checkpoint/artifacts/hasher.ts +6 -0
- package/src/checkpoint/artifacts/manifest.ts +72 -0
- package/src/checkpoint/artifacts/materializer.ts +211 -0
- package/src/checkpoint/artifacts/packager.ts +100 -0
- package/src/checkpoint/artifacts/repository.ts +36 -0
- package/src/checkpoint/artifacts/store.ts +102 -0
- package/src/checkpoint/artifacts/types.ts +24 -0
- package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
- package/src/checkpoint/asset-store.ts +61 -0
- package/src/checkpoint/bundler.ts +64 -0
- package/src/checkpoint/cli.ts +177 -0
- package/src/checkpoint/entrypoint-generator.ts +275 -0
- package/src/checkpoint/forwarder.ts +84 -0
- package/src/checkpoint/gateway.ts +40 -0
- package/src/checkpoint/ipc.ts +107 -0
- package/src/checkpoint/manager.ts +254 -0
- package/src/checkpoint/process-manager.ts +250 -0
- package/src/checkpoint/resolver.ts +124 -0
- package/src/checkpoint/socket-path.ts +61 -0
- package/src/checkpoint/types.ts +63 -0
- package/src/cli/index.ts +11 -2
- package/src/cli/option-resolution.ts +5 -1
- package/src/core/config-loader.ts +11 -2
- package/src/core/router.ts +505 -264
- package/src/core/server.ts +36 -9
- package/src/core/vector.ts +60 -1
- package/src/dev/route-scanner.ts +2 -1
- package/src/http.ts +219 -19
- package/src/index.ts +3 -2
- package/src/middleware/manager.ts +10 -10
- package/src/openapi/docs-ui.ts +1097 -61
- package/src/openapi/generator.ts +265 -6
- package/src/types/index.ts +83 -30
- package/src/utils/validation.ts +5 -3
package/src/core/router.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { CacheManager } from '../cache/manager';
|
|
|
3
3
|
import { APIError, createResponse } from '../http';
|
|
4
4
|
import type { MiddlewareManager } from '../middleware/manager';
|
|
5
5
|
import { STATIC_RESPONSES } from '../constants';
|
|
6
|
+
import { AuthKind } from '../types';
|
|
6
7
|
import { buildRouteRegex } from '../utils/path';
|
|
8
|
+
import type { CheckpointGateway } from '../checkpoint/gateway';
|
|
7
9
|
import type {
|
|
8
10
|
BunMethodMap,
|
|
9
11
|
BunRouteTable,
|
|
@@ -14,6 +16,7 @@ import type {
|
|
|
14
16
|
RouteHandler,
|
|
15
17
|
RouteOptions,
|
|
16
18
|
RouteSchemaDefinition,
|
|
19
|
+
VectorContext,
|
|
17
20
|
VectorRequest,
|
|
18
21
|
VectorTypes,
|
|
19
22
|
} from '../types';
|
|
@@ -25,6 +28,12 @@ import {
|
|
|
25
28
|
runStandardValidation,
|
|
26
29
|
} from '../utils/schema-validation';
|
|
27
30
|
|
|
31
|
+
const AUTH_KIND_VALUES = new Set<string>(Object.values(AuthKind));
|
|
32
|
+
|
|
33
|
+
function isAuthKindValue(value: unknown): value is AuthKind {
|
|
34
|
+
return typeof value === 'string' && AUTH_KIND_VALUES.has(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export interface RegisteredRouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
29
38
|
method: string;
|
|
30
39
|
path: string;
|
|
@@ -37,6 +46,11 @@ interface RouteMatcher {
|
|
|
37
46
|
specificity: number;
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
interface InputValidationResult {
|
|
50
|
+
response: Response | null;
|
|
51
|
+
requiresBody: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
41
55
|
private middlewareManager: MiddlewareManager<TTypes>;
|
|
42
56
|
private authManager: AuthManager<TTypes>;
|
|
@@ -48,6 +62,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
48
62
|
private routeMatchers: RouteMatcher[] = [];
|
|
49
63
|
private corsHeadersEntries: [string, string][] | null = null;
|
|
50
64
|
private corsHandler: ((response: Response, request: Request) => Response) | null = null;
|
|
65
|
+
private checkpointGateway: CheckpointGateway | null = null;
|
|
51
66
|
|
|
52
67
|
constructor(
|
|
53
68
|
middlewareManager: MiddlewareManager<TTypes>,
|
|
@@ -67,6 +82,10 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
67
82
|
this.corsHandler = handler;
|
|
68
83
|
}
|
|
69
84
|
|
|
85
|
+
setCheckpointGateway(gateway: CheckpointGateway | null): void {
|
|
86
|
+
this.checkpointGateway = gateway;
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
setRouteBooleanDefaults(defaults?: RouteBooleanDefaults): void {
|
|
71
90
|
this.routeBooleanDefaults = { ...defaults };
|
|
72
91
|
}
|
|
@@ -87,6 +106,12 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
109
|
+
// If a route explicitly sets auth:true and the global default auth is an AuthKind,
|
|
110
|
+
// promote the route to that kind so OpenAPI docs and runtime defaults stay aligned.
|
|
111
|
+
if (resolved.auth === true && isAuthKindValue(defaults.auth)) {
|
|
112
|
+
resolved.auth = defaults.auth;
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
return resolved;
|
|
91
116
|
}
|
|
92
117
|
|
|
@@ -181,157 +206,249 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
181
206
|
} catch {
|
|
182
207
|
return APIError.badRequest('Malformed request URL');
|
|
183
208
|
}
|
|
184
|
-
(request as any)._parsedUrl = url;
|
|
185
209
|
const pathname = url.pathname;
|
|
210
|
+
// Fast path: exact route lookup avoids scanning regex matchers for common static/method routes.
|
|
211
|
+
const exactPathRoute = this.routeTable[pathname];
|
|
212
|
+
|
|
213
|
+
if (exactPathRoute) {
|
|
214
|
+
if (exactPathRoute instanceof Response) {
|
|
215
|
+
// Route table stores a shared Response instance for static routes; clone per request.
|
|
216
|
+
return this.applyCorsResponse(exactPathRoute.clone() as unknown as Response, request);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const exactPathMethodMap = exactPathRoute as BunMethodMap;
|
|
220
|
+
const handler =
|
|
221
|
+
exactPathMethodMap[request.method] ?? (request.method === 'HEAD' ? exactPathMethodMap['GET'] : undefined);
|
|
222
|
+
|
|
223
|
+
if (handler) {
|
|
224
|
+
const response = await handler(request);
|
|
225
|
+
if (response) {
|
|
226
|
+
return response;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
186
230
|
|
|
187
231
|
for (const matcher of this.routeMatchers) {
|
|
188
232
|
const path = matcher.path;
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (match) {
|
|
196
|
-
try {
|
|
197
|
-
(request as any).params = match.groups ?? {};
|
|
198
|
-
} catch {
|
|
199
|
-
// Request.params can be readonly on Bun-native requests.
|
|
200
|
-
}
|
|
201
|
-
const handler = methodMap[request.method] ?? methodMap['GET'];
|
|
202
|
-
if (handler) {
|
|
203
|
-
const response = await handler(request);
|
|
204
|
-
if (response) return response;
|
|
205
|
-
}
|
|
233
|
+
const routeEntry = this.routeTable[path];
|
|
234
|
+
if (!routeEntry) continue;
|
|
235
|
+
if (routeEntry instanceof Response) {
|
|
236
|
+
if (pathname === path) {
|
|
237
|
+
// Same reason as exact-path static route handling above.
|
|
238
|
+
return this.applyCorsResponse(routeEntry.clone() as unknown as Response, request);
|
|
206
239
|
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const methodMap = routeEntry as BunMethodMap;
|
|
243
|
+
const handler = methodMap[request.method] ?? (request.method === 'HEAD' ? methodMap['GET'] : undefined);
|
|
244
|
+
if (!handler) {
|
|
245
|
+
continue;
|
|
207
246
|
}
|
|
247
|
+
|
|
248
|
+
const match = pathname.match(matcher.regex);
|
|
249
|
+
if (!match) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const response = await handler(request);
|
|
254
|
+
if (response) return response;
|
|
208
255
|
}
|
|
209
256
|
|
|
210
|
-
|
|
257
|
+
// STATIC_RESPONSES are shared singletons; clone before per-request header mutation.
|
|
258
|
+
return this.applyCorsResponse(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
|
|
211
259
|
}
|
|
212
260
|
|
|
213
|
-
private
|
|
261
|
+
private cloneMetadata<T>(value: T): T {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
return [...value] as unknown as T;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (value && typeof value === 'object') {
|
|
267
|
+
return { ...(value as Record<string, unknown>) } as unknown as T;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private createContext(
|
|
214
274
|
request: VectorRequest<TTypes>,
|
|
215
275
|
options?: {
|
|
216
|
-
params?: Record<string, string>;
|
|
217
|
-
route?: string;
|
|
218
276
|
metadata?: any;
|
|
277
|
+
params?: Record<string, string>;
|
|
278
|
+
query?: Record<string, string | string[]>;
|
|
279
|
+
cookies?: Record<string, string>;
|
|
280
|
+
}
|
|
281
|
+
): VectorContext<TTypes> {
|
|
282
|
+
const context = {
|
|
283
|
+
request,
|
|
284
|
+
} as VectorContext<TTypes>;
|
|
285
|
+
|
|
286
|
+
this.setContextField(
|
|
287
|
+
context,
|
|
288
|
+
'metadata',
|
|
289
|
+
options?.metadata !== undefined ? this.cloneMetadata(options.metadata) : ({} as any)
|
|
290
|
+
);
|
|
291
|
+
this.setContextField(context, 'params', options?.params ?? {});
|
|
292
|
+
this.setContextField(context, 'query', options?.query ?? {});
|
|
293
|
+
this.setContextField(context, 'cookies', options?.cookies ?? {});
|
|
294
|
+
|
|
295
|
+
return context;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private setContextField(context: VectorContext<TTypes>, key: string, value: unknown): void {
|
|
299
|
+
(context as Record<string, unknown>)[key] = value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private hasOwnContextField(context: VectorContext<TTypes>, key: string): boolean {
|
|
303
|
+
return Object.prototype.hasOwnProperty.call(context, key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private buildCheckpointContextPayload(context: VectorContext<TTypes>): Record<string, unknown> {
|
|
307
|
+
const payload: Record<string, unknown> = {};
|
|
308
|
+
const allowedKeys = ['metadata', 'content', 'validatedInput', 'authUser'] as const;
|
|
309
|
+
for (const key of allowedKeys) {
|
|
310
|
+
if (!this.hasOwnContextField(context, key)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const value = (context as Record<string, unknown>)[key];
|
|
315
|
+
if (typeof value === 'function' || typeof value === 'symbol' || value === undefined) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
payload[key] = value;
|
|
219
319
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
320
|
+
return payload;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private resolveFallbackParams(pathname: string, routeMatcher: RegExp | null): Record<string, string> | undefined {
|
|
324
|
+
if (!routeMatcher) {
|
|
325
|
+
return undefined;
|
|
223
326
|
}
|
|
224
327
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Object.keys(request.params as Record<string, unknown>).length === 0;
|
|
328
|
+
const matched = pathname.match(routeMatcher);
|
|
329
|
+
if (!matched?.groups) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
230
332
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
333
|
+
return matched.groups as Record<string, string>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private getRequestedCheckpointVersion(request: Request): string | null {
|
|
337
|
+
if (!this.checkpointGateway) {
|
|
338
|
+
return null;
|
|
237
339
|
}
|
|
238
|
-
|
|
239
|
-
|
|
340
|
+
|
|
341
|
+
const gateway = this.checkpointGateway as unknown as {
|
|
342
|
+
getRequestedVersion?: (request: Request) => string | null;
|
|
343
|
+
} | null;
|
|
344
|
+
if (gateway?.getRequestedVersion) {
|
|
345
|
+
return gateway.getRequestedVersion(request);
|
|
240
346
|
}
|
|
241
|
-
|
|
242
|
-
|
|
347
|
+
|
|
348
|
+
const primary = request.headers.get('x-vector-checkpoint-version');
|
|
349
|
+
if (primary && primary.trim().length > 0) {
|
|
350
|
+
return primary.trim();
|
|
243
351
|
}
|
|
244
352
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
get() {
|
|
249
|
-
const url = (this as any)._parsedUrl ?? new URL(this.url);
|
|
250
|
-
const query = VectorRouter.parseQuery(url);
|
|
251
|
-
Object.defineProperty(this, 'query', {
|
|
252
|
-
value: query,
|
|
253
|
-
writable: true,
|
|
254
|
-
configurable: true,
|
|
255
|
-
enumerable: true,
|
|
256
|
-
});
|
|
257
|
-
return query;
|
|
258
|
-
},
|
|
259
|
-
set(value) {
|
|
260
|
-
Object.defineProperty(this, 'query', {
|
|
261
|
-
value,
|
|
262
|
-
writable: true,
|
|
263
|
-
configurable: true,
|
|
264
|
-
enumerable: true,
|
|
265
|
-
});
|
|
266
|
-
},
|
|
267
|
-
configurable: true,
|
|
268
|
-
enumerable: true,
|
|
269
|
-
});
|
|
270
|
-
} catch {
|
|
271
|
-
const url = (request as any)._parsedUrl ?? new URL(request.url);
|
|
272
|
-
try {
|
|
273
|
-
request.query = VectorRouter.parseQuery(url);
|
|
274
|
-
} catch {
|
|
275
|
-
// Leave query as-is when request shape is non-extensible.
|
|
276
|
-
}
|
|
277
|
-
}
|
|
353
|
+
const fallback = request.headers.get('x-vector-checkpoint');
|
|
354
|
+
if (fallback && fallback.trim().length > 0) {
|
|
355
|
+
return fallback.trim();
|
|
278
356
|
}
|
|
279
357
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private getCheckpointCacheKeyOverrideValue(request: Request): string | null {
|
|
362
|
+
if (!this.checkpointGateway) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const gateway = this.checkpointGateway as unknown as {
|
|
367
|
+
getCacheKeyOverrideValue?: (request: Request) => string | null;
|
|
368
|
+
} | null;
|
|
369
|
+
if (gateway?.getCacheKeyOverrideValue) {
|
|
370
|
+
return gateway.getCacheKeyOverrideValue(request);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const primary = request.headers.get('x-vector-checkpoint-version');
|
|
374
|
+
if (primary && primary.trim().length > 0) {
|
|
375
|
+
return `x-vector-checkpoint-version:${primary.trim()}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const fallback = request.headers.get('x-vector-checkpoint');
|
|
379
|
+
if (fallback && fallback.trim().length > 0) {
|
|
380
|
+
return `x-vector-checkpoint:${fallback.trim()}`;
|
|
304
381
|
}
|
|
382
|
+
|
|
383
|
+
return null;
|
|
305
384
|
}
|
|
306
385
|
|
|
307
|
-
private
|
|
308
|
-
|
|
309
|
-
|
|
386
|
+
private applyCheckpointCacheNamespace(cacheKey: string, request: Request): string {
|
|
387
|
+
const checkpointVersion = this.getRequestedCheckpointVersion(request);
|
|
388
|
+
if (!checkpointVersion) {
|
|
389
|
+
return cacheKey;
|
|
310
390
|
}
|
|
311
391
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return undefined;
|
|
392
|
+
return `${cacheKey}:checkpoint=${checkpointVersion}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private applyCheckpointRouteKeyOverride(cacheKey: string, request: Request): string {
|
|
396
|
+
const override = this.getCheckpointCacheKeyOverrideValue(request);
|
|
397
|
+
if (!override) {
|
|
398
|
+
return cacheKey;
|
|
320
399
|
}
|
|
321
400
|
|
|
322
|
-
|
|
401
|
+
return override;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private async parseRequestBodyForContext(
|
|
405
|
+
context: VectorContext<TTypes>,
|
|
406
|
+
request: Request,
|
|
407
|
+
checkpointRequested: boolean
|
|
408
|
+
): Promise<void> {
|
|
409
|
+
let parsedContent: unknown = null;
|
|
323
410
|
try {
|
|
324
|
-
|
|
411
|
+
// For checkpoint requests we may forward the original stream later, so parse from a clone.
|
|
412
|
+
const bodyReadRequest = checkpointRequested ? request.clone() : request;
|
|
413
|
+
const contentType = bodyReadRequest.headers.get('content-type');
|
|
414
|
+
if (contentType?.startsWith('application/json')) {
|
|
415
|
+
parsedContent = await bodyReadRequest.json();
|
|
416
|
+
} else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
|
|
417
|
+
parsedContent = Object.fromEntries(await bodyReadRequest.formData());
|
|
418
|
+
} else if (contentType?.startsWith('multipart/form-data')) {
|
|
419
|
+
parsedContent = await bodyReadRequest.formData();
|
|
420
|
+
} else {
|
|
421
|
+
parsedContent = await bodyReadRequest.text();
|
|
422
|
+
}
|
|
325
423
|
} catch {
|
|
326
|
-
|
|
424
|
+
parsedContent = null;
|
|
327
425
|
}
|
|
328
426
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
427
|
+
this.setContextField(context, 'content', parsedContent);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private isLikelyStreamingBodyRequest(request: Request): boolean {
|
|
431
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
432
|
+
return false;
|
|
332
433
|
}
|
|
333
434
|
|
|
334
|
-
|
|
435
|
+
if (!request.body) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if ((request as { duplex?: unknown }).duplex === 'half') {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const transferEncoding = request.headers.get('transfer-encoding');
|
|
444
|
+
if (transferEncoding) {
|
|
445
|
+
const hasChunked = transferEncoding.split(',').some((value) => value.trim().toLowerCase() === 'chunked');
|
|
446
|
+
if (hasChunked) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return false;
|
|
335
452
|
}
|
|
336
453
|
|
|
337
454
|
private wrapHandler(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>) {
|
|
@@ -340,12 +457,18 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
340
457
|
|
|
341
458
|
return async (request: Request) => {
|
|
342
459
|
const vectorRequest = request as unknown as VectorRequest<TTypes>;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
route
|
|
460
|
+
let pathname = '';
|
|
461
|
+
try {
|
|
462
|
+
pathname = new URL(request.url).pathname;
|
|
463
|
+
} catch {
|
|
464
|
+
// Ignore malformed URLs here; router.handle() already guards route matching.
|
|
465
|
+
}
|
|
466
|
+
const fallbackParams = this.resolveFallbackParams(pathname, routeMatcher);
|
|
467
|
+
const context = this.createContext(vectorRequest, {
|
|
348
468
|
metadata: options.metadata,
|
|
469
|
+
params: this.getRequestParams(request, fallbackParams),
|
|
470
|
+
query: this.getRequestQuery(request),
|
|
471
|
+
cookies: this.getRequestCookies(request),
|
|
349
472
|
});
|
|
350
473
|
|
|
351
474
|
try {
|
|
@@ -353,15 +476,14 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
353
476
|
return APIError.forbidden('Forbidden');
|
|
354
477
|
}
|
|
355
478
|
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
return
|
|
479
|
+
const beforeResponse = await this.middlewareManager.executeBefore(context);
|
|
480
|
+
if (beforeResponse instanceof Response) {
|
|
481
|
+
return beforeResponse;
|
|
359
482
|
}
|
|
360
|
-
const req = beforeResult as VectorRequest<TTypes>;
|
|
361
483
|
|
|
362
484
|
if (options.auth) {
|
|
363
485
|
try {
|
|
364
|
-
await this.authManager.authenticate(
|
|
486
|
+
await this.authManager.authenticate(context);
|
|
365
487
|
} catch (error) {
|
|
366
488
|
return APIError.unauthorized(
|
|
367
489
|
error instanceof Error ? error.message : 'Authentication failed',
|
|
@@ -370,84 +492,93 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
370
492
|
}
|
|
371
493
|
}
|
|
372
494
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
495
|
+
const executeRoute = async (): Promise<unknown> => {
|
|
496
|
+
const req = context.request;
|
|
497
|
+
const requestForRoute = req as unknown as Request;
|
|
498
|
+
const checkpointRequested = this.getRequestedCheckpointVersion(requestForRoute) !== null;
|
|
499
|
+
// Library-wide behavior: applies to any streaming request with input schema validation enabled,
|
|
500
|
+
// regardless of whether checkpoint routing is in play.
|
|
501
|
+
const shouldDeferStreamingValidation =
|
|
502
|
+
this.isLikelyStreamingBodyRequest(requestForRoute) &&
|
|
503
|
+
options.schema?.input !== undefined &&
|
|
504
|
+
options.validate !== false;
|
|
505
|
+
|
|
506
|
+
if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD' && !shouldDeferStreamingValidation) {
|
|
507
|
+
await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (shouldDeferStreamingValidation) {
|
|
511
|
+
const validationWithoutBody = await this.validateInputSchema(context, options, fallbackParams, {
|
|
512
|
+
includeBody: false,
|
|
513
|
+
allowBodyDeferral: true,
|
|
514
|
+
});
|
|
515
|
+
if (validationWithoutBody.response) {
|
|
516
|
+
return validationWithoutBody.response;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (validationWithoutBody.requiresBody) {
|
|
520
|
+
if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
521
|
+
await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const fullValidation = await this.validateInputSchema(context, options, fallbackParams);
|
|
525
|
+
if (fullValidation.response) {
|
|
526
|
+
return fullValidation.response;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
const inputValidation = await this.validateInputSchema(context, options, fallbackParams);
|
|
531
|
+
if (inputValidation.response) {
|
|
532
|
+
return inputValidation.response;
|
|
385
533
|
}
|
|
386
|
-
} catch {
|
|
387
|
-
parsedContent = null;
|
|
388
534
|
}
|
|
389
|
-
this.setContentAndBodyAlias(req, parsedContent);
|
|
390
|
-
}
|
|
391
535
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
536
|
+
if (this.checkpointGateway) {
|
|
537
|
+
const checkpointResponse = await this.checkpointGateway.handle(
|
|
538
|
+
req as unknown as Request,
|
|
539
|
+
this.buildCheckpointContextPayload(context)
|
|
540
|
+
);
|
|
541
|
+
if (checkpointResponse) {
|
|
542
|
+
return checkpointResponse;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return await handler(context as any);
|
|
547
|
+
};
|
|
396
548
|
|
|
397
|
-
let result;
|
|
549
|
+
let result: any;
|
|
398
550
|
const cacheOptions = options.cache;
|
|
399
551
|
|
|
400
552
|
if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
|
|
401
|
-
const cacheKey = this.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
async () => {
|
|
407
|
-
const res = await handler(req);
|
|
408
|
-
if (res instanceof Response) {
|
|
409
|
-
return {
|
|
410
|
-
_isResponse: true,
|
|
411
|
-
body: await res.text(),
|
|
412
|
-
status: res.status,
|
|
413
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
return res;
|
|
417
|
-
},
|
|
418
|
-
cacheOptions
|
|
553
|
+
const cacheKey = this.applyCheckpointCacheNamespace(
|
|
554
|
+
this.cacheManager.generateKey(context.request as any, {
|
|
555
|
+
authUser: context.authUser,
|
|
556
|
+
}),
|
|
557
|
+
context.request as unknown as Request
|
|
419
558
|
);
|
|
559
|
+
result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions);
|
|
420
560
|
} else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
561
|
+
const hasRouteCacheKey = typeof cacheOptions.key === 'string' && cacheOptions.key.length > 0;
|
|
562
|
+
let cacheKey: string;
|
|
563
|
+
if (hasRouteCacheKey) {
|
|
564
|
+
cacheKey = this.applyCheckpointRouteKeyOverride(
|
|
565
|
+
cacheOptions.key as string,
|
|
566
|
+
context.request as unknown as Request
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
569
|
+
const generatedKey = this.cacheManager.generateKey(context.request as any, {
|
|
570
|
+
authUser: context.authUser,
|
|
425
571
|
});
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const res = await handler(req);
|
|
430
|
-
if (res instanceof Response) {
|
|
431
|
-
return {
|
|
432
|
-
_isResponse: true,
|
|
433
|
-
body: await res.text(),
|
|
434
|
-
status: res.status,
|
|
435
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
return res;
|
|
439
|
-
},
|
|
440
|
-
cacheOptions.ttl
|
|
441
|
-
);
|
|
572
|
+
cacheKey = this.applyCheckpointCacheNamespace(generatedKey, context.request as unknown as Request);
|
|
573
|
+
}
|
|
574
|
+
result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions.ttl);
|
|
442
575
|
} else {
|
|
443
|
-
result = await
|
|
576
|
+
result = await executeRoute();
|
|
444
577
|
}
|
|
445
578
|
|
|
446
|
-
if (result
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
headers: result.headers,
|
|
450
|
-
});
|
|
579
|
+
if (result instanceof Response && !!cacheOptions) {
|
|
580
|
+
// Cache layers can return shared Response instances; clone before per-request mutations.
|
|
581
|
+
result = result.clone();
|
|
451
582
|
}
|
|
452
583
|
|
|
453
584
|
let response: Response;
|
|
@@ -457,22 +588,9 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
457
588
|
response = createResponse(200, result, options.responseContentType);
|
|
458
589
|
}
|
|
459
590
|
|
|
460
|
-
response = await this.middlewareManager.executeFinally(response,
|
|
461
|
-
|
|
462
|
-
// Apply pre-built CORS headers if configured
|
|
463
|
-
const entries = this.corsHeadersEntries;
|
|
464
|
-
if (entries) {
|
|
465
|
-
for (const [k, v] of entries) {
|
|
466
|
-
response.headers.set(k, v);
|
|
467
|
-
}
|
|
468
|
-
} else {
|
|
469
|
-
const dynamicCors = this.corsHandler;
|
|
470
|
-
if (dynamicCors) {
|
|
471
|
-
response = dynamicCors(response, req as unknown as Request);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
591
|
+
response = await this.middlewareManager.executeFinally(response, context);
|
|
474
592
|
|
|
475
|
-
return response;
|
|
593
|
+
return this.applyCorsResponse(response, context.request as unknown as Request);
|
|
476
594
|
} catch (error) {
|
|
477
595
|
if (error instanceof Response) {
|
|
478
596
|
return error;
|
|
@@ -497,13 +615,18 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
497
615
|
}
|
|
498
616
|
|
|
499
617
|
private async buildInputValidationPayload(
|
|
500
|
-
|
|
501
|
-
options: RouteOptions<TTypes
|
|
618
|
+
context: VectorContext<TTypes>,
|
|
619
|
+
options: RouteOptions<TTypes>,
|
|
620
|
+
fallbackParams?: Record<string, string>,
|
|
621
|
+
validationOptions?: { includeBody?: boolean }
|
|
502
622
|
): Promise<Record<string, unknown>> {
|
|
503
|
-
|
|
623
|
+
const request = context.request;
|
|
624
|
+
const includeBody = validationOptions?.includeBody !== false;
|
|
625
|
+
let body = includeBody && this.hasOwnContextField(context, 'content') ? context.content : undefined;
|
|
504
626
|
|
|
505
|
-
if (options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
627
|
+
if (includeBody && options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
506
628
|
try {
|
|
629
|
+
// Read raw body from a clone so handlers/checkpoint forwarding can still consume the original stream.
|
|
507
630
|
body = await (request as unknown as Request).clone().text();
|
|
508
631
|
} catch {
|
|
509
632
|
body = null;
|
|
@@ -511,109 +634,194 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
511
634
|
}
|
|
512
635
|
|
|
513
636
|
return {
|
|
514
|
-
params: request
|
|
515
|
-
query: request
|
|
637
|
+
params: this.getRequestParams(request as unknown as Request, fallbackParams),
|
|
638
|
+
query: this.getRequestQuery(request as unknown as Request),
|
|
516
639
|
headers: Object.fromEntries(request.headers.entries()),
|
|
517
|
-
cookies: request
|
|
640
|
+
cookies: this.getRequestCookies(request as unknown as Request),
|
|
518
641
|
body,
|
|
519
642
|
};
|
|
520
643
|
}
|
|
521
644
|
|
|
522
|
-
private
|
|
523
|
-
|
|
645
|
+
private getRequestParams(request: Request, fallbackParams?: Record<string, string>): Record<string, string> {
|
|
646
|
+
const nativeParams = this.readRequestObjectField(request, 'params');
|
|
647
|
+
if (nativeParams && Object.keys(nativeParams).length > 0) {
|
|
648
|
+
return nativeParams as Record<string, string>;
|
|
649
|
+
}
|
|
650
|
+
return fallbackParams ?? {};
|
|
651
|
+
}
|
|
524
652
|
|
|
525
|
-
|
|
526
|
-
|
|
653
|
+
private getRequestQuery(request: Request): Record<string, string | string[]> {
|
|
654
|
+
const nativeQuery = this.readRequestObjectField(request, 'query');
|
|
655
|
+
if (nativeQuery) {
|
|
656
|
+
return nativeQuery as Record<string, string | string[]>;
|
|
527
657
|
}
|
|
528
658
|
|
|
529
|
-
|
|
659
|
+
try {
|
|
660
|
+
return VectorRouter.parseQuery(new URL(request.url));
|
|
661
|
+
} catch {
|
|
662
|
+
return {};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
530
665
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// Request.params can be readonly on Bun-native requests.
|
|
536
|
-
}
|
|
666
|
+
private getRequestCookies(request: Request): Record<string, string> {
|
|
667
|
+
const nativeCookies = this.readRequestObjectField(request, 'cookies');
|
|
668
|
+
if (nativeCookies) {
|
|
669
|
+
return nativeCookies as Record<string, string>;
|
|
537
670
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
671
|
+
|
|
672
|
+
return VectorRouter.parseCookies(request.headers.get('cookie'));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private readRequestObjectField(request: Request, key: string): Record<string, unknown> | undefined {
|
|
676
|
+
const value = (request as any)[key];
|
|
677
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
678
|
+
return undefined;
|
|
544
679
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
680
|
+
return value as Record<string, unknown>;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private applyValidatedInput(context: VectorContext<TTypes>, validatedValue: unknown): void {
|
|
684
|
+
this.setContextField(context, 'validatedInput', validatedValue as any);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private issueHasBodyPath(issue: unknown): boolean {
|
|
688
|
+
if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
|
|
689
|
+
return false;
|
|
551
690
|
}
|
|
552
|
-
|
|
553
|
-
|
|
691
|
+
|
|
692
|
+
const path = (issue as { path?: unknown }).path;
|
|
693
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const segment = path[0];
|
|
698
|
+
if (segment && typeof segment === 'object' && 'key' in (segment as Record<string, unknown>)) {
|
|
699
|
+
return (segment as { key?: unknown }).key === 'body';
|
|
554
700
|
}
|
|
701
|
+
return segment === 'body';
|
|
555
702
|
}
|
|
556
703
|
|
|
557
|
-
private
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
} catch {
|
|
561
|
-
// Request.content can be readonly/non-configurable on some request objects.
|
|
562
|
-
return;
|
|
704
|
+
private issueHasExplicitNonBodyPath(issue: unknown): boolean {
|
|
705
|
+
if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
|
|
706
|
+
return false;
|
|
563
707
|
}
|
|
564
708
|
|
|
565
|
-
|
|
709
|
+
const path = (issue as { path?: unknown }).path;
|
|
710
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const segment = path[0];
|
|
715
|
+
if (segment && typeof segment === 'object' && 'key' in (segment as Record<string, unknown>)) {
|
|
716
|
+
return (segment as { key?: unknown }).key !== 'body';
|
|
717
|
+
}
|
|
718
|
+
return segment !== 'body';
|
|
566
719
|
}
|
|
567
720
|
|
|
568
|
-
private
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
721
|
+
private issueHasUnknownPath(issue: unknown): boolean {
|
|
722
|
+
if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const path = (issue as { path?: unknown }).path;
|
|
727
|
+
if (!Array.isArray(path)) {
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return path.length === 0;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private shouldDeferBodyValidation(
|
|
735
|
+
issues: readonly unknown[],
|
|
736
|
+
context: VectorContext<TTypes>,
|
|
737
|
+
validationOptions?: { includeBody?: boolean; allowBodyDeferral?: boolean }
|
|
738
|
+
): boolean {
|
|
739
|
+
if (!(validationOptions?.allowBodyDeferral === true && validationOptions?.includeBody === false)) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const request = context.request as unknown as Request;
|
|
744
|
+
const mayHaveRequestBody = request.method !== 'GET' && request.method !== 'HEAD' && request.body !== null;
|
|
745
|
+
if (!mayHaveRequestBody || issues.length === 0) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (issues.some((issue) => this.issueHasBodyPath(issue))) {
|
|
750
|
+
return true;
|
|
573
751
|
}
|
|
752
|
+
|
|
753
|
+
// Conservative fallback: if issues do not identify a non-body target and at least one issue
|
|
754
|
+
// has unknown/empty path, retry once with body included.
|
|
755
|
+
const hasExplicitNonBodyPath = issues.some((issue) => this.issueHasExplicitNonBodyPath(issue));
|
|
756
|
+
const hasUnknownPath = issues.some((issue) => this.issueHasUnknownPath(issue));
|
|
757
|
+
return !hasExplicitNonBodyPath && hasUnknownPath;
|
|
574
758
|
}
|
|
575
759
|
|
|
576
760
|
private async validateInputSchema(
|
|
577
|
-
|
|
578
|
-
options: RouteOptions<TTypes
|
|
579
|
-
|
|
761
|
+
context: VectorContext<TTypes>,
|
|
762
|
+
options: RouteOptions<TTypes>,
|
|
763
|
+
fallbackParams?: Record<string, string>,
|
|
764
|
+
validationOptions?: { includeBody?: boolean; allowBodyDeferral?: boolean }
|
|
765
|
+
): Promise<InputValidationResult> {
|
|
580
766
|
const inputSchema = options.schema?.input;
|
|
581
767
|
|
|
582
768
|
if (!inputSchema) {
|
|
583
|
-
return null;
|
|
769
|
+
return { response: null, requiresBody: false };
|
|
584
770
|
}
|
|
585
771
|
|
|
586
772
|
if (options.validate === false) {
|
|
587
|
-
return null;
|
|
773
|
+
return { response: null, requiresBody: false };
|
|
588
774
|
}
|
|
589
775
|
|
|
590
776
|
if (!isStandardRouteSchema(inputSchema)) {
|
|
591
|
-
return
|
|
777
|
+
return {
|
|
778
|
+
response: APIError.internalServerError('Invalid route schema configuration', options.responseContentType),
|
|
779
|
+
requiresBody: false,
|
|
780
|
+
};
|
|
592
781
|
}
|
|
593
782
|
|
|
594
783
|
const includeRawIssues = this.isDevelopmentMode();
|
|
595
|
-
const payload = await this.buildInputValidationPayload(
|
|
784
|
+
const payload = await this.buildInputValidationPayload(context, options, fallbackParams, {
|
|
785
|
+
includeBody: validationOptions?.includeBody,
|
|
786
|
+
});
|
|
596
787
|
|
|
597
788
|
try {
|
|
598
789
|
const validation = await runStandardValidation(inputSchema, payload);
|
|
599
790
|
if (validation.success === false) {
|
|
791
|
+
if (this.shouldDeferBodyValidation(validation.issues, context, validationOptions)) {
|
|
792
|
+
return { response: null, requiresBody: true };
|
|
793
|
+
}
|
|
794
|
+
|
|
600
795
|
const issues = normalizeValidationIssues(validation.issues, includeRawIssues);
|
|
601
|
-
return
|
|
796
|
+
return {
|
|
797
|
+
response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
|
|
798
|
+
requiresBody: false,
|
|
799
|
+
};
|
|
602
800
|
}
|
|
603
801
|
|
|
604
|
-
this.applyValidatedInput(
|
|
605
|
-
return null;
|
|
802
|
+
this.applyValidatedInput(context, validation.value);
|
|
803
|
+
return { response: null, requiresBody: false };
|
|
606
804
|
} catch (error) {
|
|
607
805
|
const thrownIssues = extractThrownIssues(error);
|
|
608
806
|
if (thrownIssues) {
|
|
807
|
+
if (this.shouldDeferBodyValidation(thrownIssues, context, validationOptions)) {
|
|
808
|
+
return { response: null, requiresBody: true };
|
|
809
|
+
}
|
|
810
|
+
|
|
609
811
|
const issues = normalizeValidationIssues(thrownIssues, includeRawIssues);
|
|
610
|
-
return
|
|
812
|
+
return {
|
|
813
|
+
response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
|
|
814
|
+
requiresBody: false,
|
|
815
|
+
};
|
|
611
816
|
}
|
|
612
817
|
|
|
613
|
-
return
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
818
|
+
return {
|
|
819
|
+
response: APIError.internalServerError(
|
|
820
|
+
error instanceof Error ? error.message : 'Validation failed',
|
|
821
|
+
options.responseContentType
|
|
822
|
+
),
|
|
823
|
+
requiresBody: false,
|
|
824
|
+
};
|
|
617
825
|
}
|
|
618
826
|
}
|
|
619
827
|
|
|
@@ -672,6 +880,22 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
672
880
|
return query;
|
|
673
881
|
}
|
|
674
882
|
|
|
883
|
+
private static parseCookies(cookieHeader: string | null): Record<string, string> {
|
|
884
|
+
const cookies: Record<string, string> = {};
|
|
885
|
+
if (!cookieHeader) {
|
|
886
|
+
return cookies;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
for (const pair of cookieHeader.split(';')) {
|
|
890
|
+
const idx = pair.indexOf('=');
|
|
891
|
+
if (idx > 0) {
|
|
892
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return cookies;
|
|
897
|
+
}
|
|
898
|
+
|
|
675
899
|
private routeSpecificityScore(path: string): number {
|
|
676
900
|
const STATIC_SEGMENT_WEIGHT = 1000;
|
|
677
901
|
const PARAM_SEGMENT_WEIGHT = 10;
|
|
@@ -698,4 +922,21 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
|
698
922
|
|
|
699
923
|
return score;
|
|
700
924
|
}
|
|
925
|
+
|
|
926
|
+
private applyCorsResponse(response: Response, request: Request): Response {
|
|
927
|
+
const entries = this.corsHeadersEntries;
|
|
928
|
+
if (entries) {
|
|
929
|
+
for (const [k, v] of entries) {
|
|
930
|
+
response.headers.set(k, v);
|
|
931
|
+
}
|
|
932
|
+
return response;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const dynamicCors = this.corsHandler;
|
|
936
|
+
if (dynamicCors) {
|
|
937
|
+
return dynamicCors(response, request);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return response;
|
|
941
|
+
}
|
|
701
942
|
}
|